Support ws (#3291)

* [Update] add ws support

* [Update] 修改log使用websocket

* [Update] 修复 资产用户 右侧动作菜单,字体颜色

* [Update] 修改Dockerfile

* [Bugfix] 修复settings中WSG_APPLICATION
pull/3293/head
老广 2019-09-26 19:22:17 +08:00 committed by GitHub
parent e35ba52236
commit cff009e758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 171 additions and 105 deletions

View File

@ -22,4 +22,5 @@ ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8 ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081
ENTRYPOINT ["./entrypoint.sh"] ENTRYPOINT ["./entrypoint.sh"]

View File

@ -1,9 +1,14 @@
{% load i18n %} {% load i18n %}
<style> <style>
.btn-group>.btn+.dropdown-toggle { .btn-group>.btn+.dropdown-toggle {
padding-right: 4px; padding-right: 4px;
padding-left: 4px; padding-left: 4px;
} }
table.dataTable tbody tr.selected a {
color: rgb(103, 106, 108);;
}
</style> </style>
<table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%"> <table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%">
<thead> <thead>
@ -137,8 +142,7 @@ $(document).ready(function(){
} }
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
@ -149,4 +153,4 @@ $(document).ready(function(){
}) })
</script> </script>

View File

@ -85,8 +85,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}"; var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,

View File

@ -81,8 +81,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}"; var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
@ -92,4 +91,4 @@ $(document).ready(function () {
}); });
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -276,8 +276,7 @@ function refreshAssetHardware() {
var success = function(data) { var success = function(data) {
console.log(data); console.log(data);
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
@ -355,8 +354,7 @@ $(document).ready(function () {
var success = function(data) { var success = function(data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600')
}; };
requestApi({ requestApi({

View File

@ -523,8 +523,7 @@ $(document).ready(function(){
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600')
} }
requestApi({ requestApi({
url: the_url, url: the_url,
@ -538,8 +537,7 @@ $(document).ready(function(){
function success(data) { function success(data) {
rMenu.css({"visibility" : "hidden"}); rMenu.css({"visibility" : "hidden"});
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600')
} }
requestApi({ requestApi({
url: the_url, url: the_url,
@ -552,4 +550,4 @@ $(document).ready(function(){
</script> </script>
{% endblock %} {% endblock %}

View File

@ -202,8 +202,7 @@ $(document).ready(function () {
}; };
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
@ -219,8 +218,7 @@ $(document).ready(function () {
the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id); the_url = the_url.replace("{{ DEFAULT_PK }}", asset_id);
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
var error = function (data) { var error = function (data) {
alert(data) alert(data)
@ -239,8 +237,7 @@ $(document).ready(function () {
}; };
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,

View File

@ -251,8 +251,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-push' pk=system_user.id %}";
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,
@ -265,8 +264,7 @@ $(document).ready(function () {
var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}"; var the_url = "{% url 'api-assets:system-user-connective' pk=system_user.id %}";
var success = function (data) { var success = function (data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,

View File

@ -83,6 +83,8 @@ class LogTailApi(generics.RetrieveAPIView):
return Response({"data": data, 'end': end, 'mark': new_mark}) return Response({"data": data, 'end': end, 'mark': new_mark})
class ResourcesIDCacheApi(APIView): class ResourcesIDCacheApi(APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
spm = str(uuid.uuid4()) spm = str(uuid.uuid4())

7
apps/jumpserver/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", "jumpserver.settings")
django.setup()
application = get_default_application()

View File

@ -335,6 +335,7 @@ defaults = {
'REDIS_DB_CELERY': 3, 'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4, 'REDIS_DB_CACHE': 4,
'REDIS_DB_SESSION': 5, 'REDIS_DB_SESSION': 5,
'REDIS_DB_WS': 6,
'CAPTCHA_TEST_MODE': None, 'CAPTCHA_TEST_MODE': None,
'TOKEN_EXPIRATION': 3600 * 24, 'TOKEN_EXPIRATION': 3600 * 24,
'DISPLAY_PER_PAGE': 25, 'DISPLAY_PER_PAGE': 25,

View File

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

View File

@ -74,6 +74,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'rest_framework_swagger',
'drf_yasg', 'drf_yasg',
'channels',
'django_filters', 'django_filters',
'bootstrap3', 'bootstrap3',
'captcha', '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_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('authentication:login') 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 PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
FLOWER_URL = CONFIG.FLOWER_URL 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],
},
},
}

View File

@ -66,6 +66,7 @@ urlpatterns = [
re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api), re_path('api/(?P<app>\w+)/(?P<version>v\d)/.*', views.redirect_format_api),
path('api/health/', views.HealthCheckView.as_view(), name="health"), path('api/health/', views.HealthCheckView.as_view(), name="health"),
path('luna/', views.LunaView.as_view(), name='luna-view'), path('luna/', views.LunaView.as_view(), name='luna-view'),
re_path('ws/.*', views.WsView.as_view(), name='ws-view'),
path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'), path('i18n/<str:lang>/', views.I18NView.as_view(), name='i18n-switch'),
path('settings/', include('settings.urls.view_urls', namespace='settings')), path('settings/', include('settings.urls.view_urls', namespace='settings')),

View File

@ -226,4 +226,11 @@ class HealthCheckView(APIView):
return JsonResponse({"status": 1, "time": int(time.time())}) 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})

View File

@ -196,8 +196,7 @@ $(document).ready(function () {
alert("没有运行历史"); alert("没有运行历史");
return return
} }
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk); showCeleryTaskLog(history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -145,8 +145,8 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
}).on('click', '.celery-task-log', function () { }).on('click', '.celery-task-log', function () {
var url = '{% url 'ops:celery-task-log' pk=object.pk %}'; var taskId = "{{ object.pk }}";
window.open(url, '', 'width=800,height=600,left=400,top=400') showCeleryTaskLog(taskId);
}) })
</script> </script>

View File

@ -20,50 +20,13 @@
</div> </div>
<script> <script>
var rowHeight = 18; var scheme = document.location.protocol === "https:" ? "wss" : "ws";
var colWidth = 10; var port = document.location.port ? ":" + document.location.port : "";
var mark = ''; var url = "/ws/ops/tasks/" + "{{ task_id }}" + "/log/";
var url = "{% url 'api-ops:celery-task-log' pk=task_id %}"; var wsURL = scheme + "://" + document.location.hostname + port + url;
var term; var term;
var end = false; var ws;
var error = false;
var interval = 200;
var success = true;
function calWinSize() {
var t = $('#marker');
{#rowHeight = 1.00 * t.height();#}
{#colWidth = 1.00 * t.width() / 6;#}
}
function resize() {
{#var rows = Math.floor(window.innerHeight / rowHeight) - 1;#}
{#var cols = Math.floor(window.innerWidth / colWidth) - 2;#}
{#term.resize(cols, rows);#}
}
function requestAndWrite() {
if (!end && success) {
success = false;
$.ajax({
url: url + '?mark=' + mark,
method: "GET",
contentType: "application/json; charset=utf-8"
}).done(function(data, textStatue, jqXHR) {
success = true;
if (jqXHR.status === 203) {
error = true;
term.write('.');
interval = 500;
}
if (jqXHR.status === 200){
term.write(data.data);
mark = data.mark;
if (data.end){
end = true
}
}
})
}
}
$(document).ready(function () { $(document).ready(function () {
term = new Terminal({ term = new Terminal({
cursorBlink: false, cursorBlink: false,
@ -74,18 +37,14 @@
disableStdin: true disableStdin: true
}); });
term.open(document.getElementById('term')); term.open(document.getElementById('term'));
term.resize(90, 32); term.resize(120, 30);
resize(); ws = new WebSocket(wsURL);
term.on('data', function (data) { ws.onmessage = function(e) {
{#term.write(data.replace('\r', '\r\n'))#} var data = JSON.parse(e.data);
term.write(data); term.write(data.message);
});
window.onresize = function () {
resize()
}; };
{#$('.terminal').detach().appendTo('#term');#} ws.onerror = function (e) {
setInterval(function () { term.write("Connect websocket server error")
requestAndWrite() }
}, interval)
}); });
</script> </script>

View File

@ -130,8 +130,7 @@ $(document).ready(function () {
alert("没有运行历史"); alert("没有运行历史");
return return
} }
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk); showCeleryTaskLog(history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}) })
</script> </script>
{% endblock %} {% endblock %}

View File

@ -174,8 +174,7 @@ $(document).ready(function () {
alert("没有运行历史"); alert("没有运行历史");
return return
} }
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk); showCeleryTaskLog(history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}) })
</script> </script>

View File

@ -155,8 +155,7 @@ $(document).ready(function () {
alert("没有运行历史"); alert("没有运行历史");
return return
} }
var url = '{% url 'ops:celery-task-log' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', history_pk); showCeleryTaskLog(history_pk);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}) })
</script> </script>

View File

@ -98,8 +98,7 @@ $(document).ready(function () {
}; };
var success = function(data) { var success = function(data) {
var task_id = data.task; var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id); showCeleryTaskLog(task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
}; };
requestApi({ requestApi({
url: the_url, url: the_url,

9
apps/ops/urls/ws_urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.urls import path
from .. import ws
app_name = 'ops'
urlpatterns = [
path('ws/ops/tasks/<uuid:task_id>/log/', ws.CeleryLogWebsocket, name='task-log-ws'),
]

41
apps/ops/ws.py Normal file
View File

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

View File

@ -88,7 +88,7 @@ table.dataTable tbody td.selected a,
table.dataTable tbody tr.selected td i.text-navy, table.dataTable tbody tr.selected td i.text-navy,
table.dataTable tbody th.selected td i.text-navy, table.dataTable tbody th.selected td i.text-navy,
table.dataTable tbody td.selected td i.text-navy { table.dataTable tbody td.selected td i.text-navy {
color: white !important; color: white;
} }
.m-0 { .m-0 {
@ -473,4 +473,4 @@ span.select2-selection__placeholder {
.p-r-5 { .p-r-5 {
padding-right: 5px; padding-right: 5px;
} }

View File

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

22
jms
View File

@ -47,6 +47,7 @@ LOG_DIR = os.path.join(BASE_DIR, 'logs')
TMP_DIR = os.path.join(BASE_DIR, 'tmp') TMP_DIR = os.path.join(BASE_DIR, 'tmp')
HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1'
HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080
WS_PORT = HTTP_PORT + 1
DEBUG = CONFIG.DEBUG or False DEBUG = CONFIG.DEBUG or False
LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO' LOG_LEVEL = CONFIG.LOG_LEVEL or 'INFO'
@ -201,12 +202,15 @@ def is_running(s, unlink=True):
def parse_service(s): def parse_service(s):
all_services = [ all_services = [
'gunicorn', 'celery_ansible', 'celery_default', 'beat', 'flower' 'gunicorn', 'celery_ansible', 'celery_default',
'beat', 'flower', 'daphne',
] ]
if s == 'all': if s == 'all':
return all_services return all_services
elif s == "web": elif s == "web":
return ['gunicorn', 'flower'] return ['gunicorn', 'flower', 'daphne']
elif s == 'ws':
return ['daphne']
elif s == "task": elif s == "task":
return ["celery_ansible", "celery_default", "beat"] return ["celery_ansible", "celery_default", "beat"]
elif s == 'gunicorn': elif s == 'gunicorn':
@ -225,10 +229,8 @@ def parse_service(s):
def get_start_gunicorn_kwargs(): def get_start_gunicorn_kwargs():
print("\n- Start Gunicorn WSGI HTTP Server") print("\n- Start Gunicorn WSGI HTTP Server")
prepare() prepare()
service = 'gunicorn'
bind = '{}:{}'.format(HTTP_HOST, HTTP_PORT) bind = '{}:{}'.format(HTTP_HOST, HTTP_PORT)
log_format = '%(h)s %(t)s "%(r)s" %(s)s %(b)s ' log_format = '%(h)s %(t)s "%(r)s" %(s)s %(b)s '
pid_file = get_pid_file_path(service)
cmd = [ cmd = [
'gunicorn', 'jumpserver.wsgi', 'gunicorn', 'jumpserver.wsgi',
@ -238,7 +240,6 @@ def get_start_gunicorn_kwargs():
'-w', str(WORKERS), '-w', str(WORKERS),
'--max-requests', '4096', '--max-requests', '4096',
'--access-logformat', log_format, '--access-logformat', log_format,
'-p', pid_file,
'--access-logfile', '-' '--access-logfile', '-'
] ]
@ -247,6 +248,16 @@ def get_start_gunicorn_kwargs():
return {'cmd': cmd, 'cwd': APPS_DIR} 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(): def get_start_celery_ansible_kwargs():
print("\n- Start Celery as Distributed Task Queue: Ansible") print("\n- Start Celery as Distributed Task Queue: Ansible")
return get_start_worker_kwargs('ansible', 4) return get_start_worker_kwargs('ansible', 4)
@ -362,6 +373,7 @@ def start_service(s):
"celery_default": get_start_celery_default_kwargs, "celery_default": get_start_celery_default_kwargs,
"beat": get_start_beat_kwargs, "beat": get_start_beat_kwargs,
"flower": get_start_flower_kwargs, "flower": get_start_flower_kwargs,
"daphne": get_start_daphne_kwargs,
} }
kwargs = services_kwargs.get(s)() kwargs = services_kwargs.get(s)()

View File

@ -86,3 +86,6 @@ httpsig==1.3.0
treelib==1.5.3 treelib==1.5.3
django-proxy==1.2.1 django-proxy==1.2.1
flower==0.9.3 flower==0.9.3
channels-redis==2.4.0
channels==2.3.0
daphne==2.3.0