Merge pull request #7391 from jumpserver/dev

v2.17.0 rc3
pull/7408/head
Jiangjie.Bai 2021-12-14 21:58:27 +08:00 committed by GitHub
commit 151d897746
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 155 additions and 126 deletions

View File

@ -138,7 +138,8 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
instance = super().get_object()
app_id = self.kwargs.get('app_id')
user_id = self.request.query_params.get("user_id")
instance.load_app_more_auth(app_id, user_id)
username = self.request.query_params.get("username")
instance.load_app_more_auth(app_id, username, user_id)
return instance

View File

@ -129,12 +129,21 @@ class AuthMixin:
if password:
self.password = password
def load_app_more_auth(self, app_id=None, user_id=None):
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# 更新用户名
from users.models import User
user = get_object_or_none(User, pk=user_id) if user_id else None
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
self.username = _username
def load_asset_special_auth(self, asset, username=''):
"""

View File

@ -40,10 +40,6 @@ def expire_node_assets_mapping_for_memory(org_id):
root_org_id = Organization.ROOT_ID
# 当前进程清除(cache 数据)
logger.debug(
"Expire node assets id mapping from cache of org={}, pid={}"
"".format(org_id, os.getpid())
)
Node.expire_node_all_asset_ids_mapping_from_cache(org_id)
Node.expire_node_all_asset_ids_mapping_from_cache(root_org_id)
@ -81,10 +77,6 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
root_org_id = Organization.ROOT_ID
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
logger.debug(
"Expire node assets id mapping from memory of org={}, pid={}"
"".format(str(org_id), os.getpid())
)
def keep_subscribe_node_assets_relation():
node_assets_mapping_for_memory_pub_sub.keep_handle_msg(handle_node_relation_change)

View File

@ -13,16 +13,24 @@ __all__ = ['add_nodes_assets_to_system_users']
@tmp_to_root_org()
def add_nodes_assets_to_system_users(nodes_keys, system_users):
from ..models import Node
from assets.tasks import push_system_user_to_assets
nodes = Node.objects.filter(key__in=nodes_keys)
assets = Node.get_nodes_all_assets(*nodes)
for system_user in system_users:
""" 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号,
无法更新节点下所有资产的管理用户的问题 """
need_push_asset_ids = []
for asset in assets:
defaults = {'asset': asset, 'systemuser': system_user, 'org_id': asset.org_id}
instance, created = AuthBook.objects.update_or_create(
defaults=defaults, asset=asset, systemuser=system_user
)
if created:
need_push_asset_ids.append(asset.id)
# # 不再自动更新资产管理用户,只允许用户手动指定。
# 只要关联都需要更新资产的管理用户
# instance.update_asset_admin_user_if_need()
if need_push_asset_ids:
push_system_user_to_assets.delay(system_user.id, need_push_asset_ids)

View File

@ -138,7 +138,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
return tasks
def get_push_windows_system_user_tasks(system_user, username=None):
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
if username is None:
username = system_user.username
password = system_user.password
@ -151,6 +151,11 @@ def get_push_windows_system_user_tasks(system_user, username=None):
if not password:
logger.error("Error: no password found")
return tasks
if system_user.ad_domain:
logger.error('System user with AD domain do not support push.')
return tasks
task = {
'name': 'Add user {}'.format(username),
'action': {

View File

@ -294,7 +294,7 @@ class SecretDetailMixin:
data.update(asset_detail)
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.id)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)

View File

@ -1,5 +1,7 @@
import copy
from urllib import parse
from django.views import View
from django.contrib import auth as auth
from django.urls import reverse
@ -23,9 +25,13 @@ logger = get_logger(__file__)
class PrepareRequestMixin:
@staticmethod
def prepare_django_request(request):
def is_secure():
url_result = parse.urlparse(settings.SITE_URL)
return 'on' if url_result.scheme == 'https' else 'off'
def prepare_django_request(self, request):
result = {
'https': 'on' if request.is_secure() else 'off',
'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'],
'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(),

View File

@ -173,7 +173,6 @@ class Cache(metaclass=CacheType):
def expire(self, *fields):
self._data = None
if not fields:
logger.debug(f'Delete cached key: key={self.key}')
self.redis.delete(self.key)
else:
self.redis.hdel(self.key, *fields)

View File

@ -170,7 +170,7 @@ class BaseService(object):
def _restart(self):
if self.retry > self.max_retry:
logging.info("Service start failed, exit: ", self.name)
logging.info("Service start failed, exit: {}".format(self.name))
self.EXIT_EVENT.set()
return
self.retry += 1

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-09 20:32+0800\n"
"POT-Creation-Date: 2021-12-14 17:54+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: JumpServer team<ibuler@qq.com>\n"
@ -343,7 +343,7 @@ msgstr "类别名称"
#: perms/serializers/application/permission.py:17
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:33
#: tickets/serializers/ticket/ticket.py:22
#: tickets/serializers/ticket/ticket.py:168
#: tickets/serializers/ticket/ticket.py:169
msgid "Type display"
msgstr "类型名称"
@ -384,10 +384,10 @@ msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:45
#: assets/serializers/system_user.py:159
#: xpack/plugins/change_auth_plan/serializers/asset.py:65
#: xpack/plugins/change_auth_plan/serializers/asset.py:68
#: xpack/plugins/change_auth_plan/serializers/asset.py:71
#: xpack/plugins/change_auth_plan/serializers/asset.py:87
#: xpack/plugins/change_auth_plan/serializers/asset.py:64
#: xpack/plugins/change_auth_plan/serializers/asset.py:67
#: xpack/plugins/change_auth_plan/serializers/asset.py:70
#: xpack/plugins/change_auth_plan/serializers/asset.py:101
#: xpack/plugins/cloud/serializers/account_attrs.py:52
msgid "This field is required."
msgstr "该字段是必填项。"
@ -586,6 +586,7 @@ msgid "Ok"
msgstr "成功"
#: assets/models/base.py:32 audits/models.py:102
#: xpack/plugins/change_auth_plan/task_handlers/base/manager.py:121
#: xpack/plugins/cloud/const.py:29
msgid "Failed"
msgstr "失败"
@ -1169,6 +1170,7 @@ msgid "Filename"
msgstr "文件名"
#: audits/models.py:42 audits/models.py:101 terminal/models/sharing.py:84
#: xpack/plugins/change_auth_plan/task_handlers/base/manager.py:119
msgid "Success"
msgstr "成功"
@ -1326,12 +1328,12 @@ msgstr ""
msgid "Auth Token"
msgstr "认证令牌"
#: audits/signals_handler.py:68 authentication/views/login.py:183
#: audits/signals_handler.py:68 authentication/views/login.py:164
#: notifications/backends/__init__.py:11 users/models/user.py:607
msgid "WeCom"
msgstr "企业微信"
#: audits/signals_handler.py:69 authentication/views/login.py:189
#: audits/signals_handler.py:69 authentication/views/login.py:170
#: notifications/backends/__init__.py:12 users/models/user.py:608
msgid "DingTalk"
msgstr "钉钉"
@ -1899,7 +1901,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
#: jumpserver/conf.py:282
#: jumpserver/conf.py:293
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: users/templates/users/_msg_account_expire_reminder.html:4
@ -2107,24 +2109,24 @@ msgstr "没有绑定飞书"
msgid "Please login with a password and then bind the FeiShu"
msgstr "请使用密码登录,然后绑定飞书"
#: authentication/views/login.py:89
#: authentication/views/login.py:70
msgid "Redirecting"
msgstr "跳转中"
#: authentication/views/login.py:90
#: authentication/views/login.py:71
msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证"
#: authentication/views/login.py:116
#: authentication/views/login.py:94
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: authentication/views/login.py:195 notifications/backends/__init__.py:14
#: authentication/views/login.py:176 notifications/backends/__init__.py:14
#: users/models/user.py:609
msgid "FeiShu"
msgstr "飞书"
#: authentication/views/login.py:284
#: authentication/views/login.py:265
msgid ""
"Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>\n"
" Don't close this page"
@ -2132,15 +2134,15 @@ msgstr ""
"等待 <b>{}</b> 确认, 你也可以复制链接发给他/她 <br/>\n"
" 不要关闭本页面"
#: authentication/views/login.py:289
#: authentication/views/login.py:270
msgid "No ticket found"
msgstr "没有发现工单"
#: authentication/views/login.py:323
#: authentication/views/login.py:304
msgid "Logout success"
msgstr "退出登录成功"
#: authentication/views/login.py:324
#: authentication/views/login.py:305
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@ -2339,11 +2341,11 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
#: jumpserver/conf.py:281
#: jumpserver/conf.py:292
msgid "Create account successfully"
msgstr "创建账户成功"
#: jumpserver/conf.py:283
#: jumpserver/conf.py:294
msgid "Your account has been created successfully"
msgstr "你的账户已创建成功"
@ -2859,7 +2861,7 @@ msgstr "服务端地址"
msgid "Proxy server url"
msgstr "回调地址"
#: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:29
#: settings/serializers/auth/cas.py:14 settings/serializers/auth/saml2.py:32
msgid "Logout completely"
msgstr "同步注销"
@ -2871,7 +2873,7 @@ msgstr "用户名属性"
msgid "Enable attributes map"
msgstr "启用属性映射"
#: settings/serializers/auth/cas.py:18 settings/serializers/auth/saml2.py:28
#: settings/serializers/auth/cas.py:18 settings/serializers/auth/saml2.py:31
msgid "Rename attr"
msgstr "映射属性"
@ -3031,7 +3033,7 @@ msgstr "使用状态"
msgid "Use nonce"
msgstr "临时使用"
#: settings/serializers/auth/oidc.py:76 settings/serializers/auth/saml2.py:30
#: settings/serializers/auth/oidc.py:76 settings/serializers/auth/saml2.py:33
msgid "Always update user"
msgstr "总是更新用户信息"
@ -3048,20 +3050,24 @@ msgid "Enable SAML2 Auth"
msgstr "启用 SAML2 认证"
#: settings/serializers/auth/saml2.py:15
msgid "IDP Metadata URL"
msgid "IDP metadata URL"
msgstr ""
#: settings/serializers/auth/saml2.py:18
msgid "IDP Metadata XML"
msgid "IDP metadata XML"
msgstr ""
#: settings/serializers/auth/saml2.py:22
msgid "SP Private Key"
msgstr ""
#: settings/serializers/auth/saml2.py:21
msgid "SP advanced settings"
msgstr "高级设置"
#: settings/serializers/auth/saml2.py:26
msgid "SP Public Cert"
msgstr ""
#: settings/serializers/auth/saml2.py:25
msgid "SP private key"
msgstr "SP 密钥"
#: settings/serializers/auth/saml2.py:29
msgid "SP cert"
msgstr "SP 证书"
#: settings/serializers/auth/sms.py:10
msgid "Enable SMS"
@ -4784,11 +4790,11 @@ msgstr "内容"
msgid "Approve level"
msgstr "审批级别"
#: tickets/models/flow.py:25 tickets/serializers/ticket/ticket.py:140
#: tickets/models/flow.py:25 tickets/serializers/ticket/ticket.py:141
msgid "Approve strategy"
msgstr "审批策略"
#: tickets/models/flow.py:30 tickets/serializers/ticket/ticket.py:141
#: tickets/models/flow.py:30 tickets/serializers/ticket/ticket.py:142
msgid "Assignees"
msgstr "受理人"
@ -4876,7 +4882,7 @@ msgstr "申请的系统用户名称"
#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:71
#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:73
#: tickets/serializers/ticket/ticket.py:127
#: tickets/serializers/ticket/ticket.py:128
msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在"
@ -4936,7 +4942,7 @@ msgid "From cmd filter"
msgstr "来自命令过滤规则"
#: tickets/serializers/ticket/meta/ticket_type/common.py:11
#: tickets/serializers/ticket/ticket.py:122
#: tickets/serializers/ticket/ticket.py:123
msgid "Created by ticket ({}-{})"
msgstr "通过工单创建 ({}-{})"
@ -4962,15 +4968,15 @@ msgid ""
"request url (`{}`)"
msgstr "提交数据中的类型 (`{}`) 与请求URL地址中的类型 (`{}`) 不一致"
#: tickets/serializers/ticket/ticket.py:115
#: tickets/serializers/ticket/ticket.py:116
msgid "The ticket flow `{}` does not exist"
msgstr "工单流程 `{}` 不存在"
#: tickets/serializers/ticket/ticket.py:162
#: tickets/serializers/ticket/ticket.py:163
msgid "Please select the Assignees"
msgstr "请选择受理人"
#: tickets/serializers/ticket/ticket.py:188
#: tickets/serializers/ticket/ticket.py:189
msgid "The current organization type already exists"
msgstr "当前组织已存在该类型"
@ -5633,7 +5639,7 @@ msgid "Replace (The key generated by JumpServer) "
msgstr "替换 (由 JumpServer 生成的密钥)"
#: xpack/plugins/change_auth_plan/models/asset.py:50
#: xpack/plugins/change_auth_plan/serializers/asset.py:34
#: xpack/plugins/change_auth_plan/serializers/asset.py:33
msgid "SSH Key strategy"
msgstr "SSH 密钥策略"
@ -5722,11 +5728,11 @@ msgstr ""
"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加"
"密密码"
#: xpack/plugins/change_auth_plan/serializers/asset.py:31
#: xpack/plugins/change_auth_plan/serializers/asset.py:30
msgid "Change Password"
msgstr "更改密码"
#: xpack/plugins/change_auth_plan/serializers/asset.py:32
#: xpack/plugins/change_auth_plan/serializers/asset.py:31
msgid "Change SSH Key"
msgstr "修改 SSH Key"
@ -5811,8 +5817,8 @@ msgid "Qingyun Private Cloud"
msgstr "青云私有云"
#: xpack/plugins/cloud/const.py:19
msgid "OpenStack Cloud"
msgstr "OpenStack Cloud"
msgid "OpenStack"
msgstr "OpenStack"
#: xpack/plugins/cloud/const.py:20
msgid "Google Cloud Platform"

View File

@ -6,6 +6,7 @@ from rest_framework import generics
from rest_framework.views import Response, APIView
from orgs.models import Organization
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from ..utils import (
LDAPServerUtil, LDAPCacheUtil, LDAPImportUtil, LDAPSyncUtil,
@ -47,6 +48,10 @@ class LDAPTestingConfigAPI(APIView):
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
auth_ldap = serializer.validated_data.get('AUTH_LDAP', False)
if not password:
password = settings.AUTH_LDAP_BIND_PASSWORD
config = {
'server_uri': server_uri,
'bind_dn': bind_dn,

View File

@ -12,21 +12,21 @@ class SAML2SettingSerializer(serializers.Serializer):
default=False, required=False, label=_('Enable SAML2 Auth')
)
SAML2_IDP_METADATA_URL = serializers.URLField(
allow_blank=True, required=False, label=_('IDP Metadata URL')
allow_blank=True, required=False, label=_('IDP metadata URL')
)
SAML2_IDP_METADATA_XML = serializers.CharField(
allow_blank=True, required=False, label=_('IDP Metadata XML')
allow_blank=True, required=False, label=_('IDP metadata XML')
)
SAML2_SP_ADVANCED_SETTINGS = serializers.JSONField(
required=False, label=_('SP ADVANCED SETTINGS')
required=False, label=_('SP advanced settings')
)
SAML2_SP_KEY_CONTENT = serializers.CharField(
allow_blank=True, required=False,
write_only=True, label=_('SP Private Key')
write_only=True, label=_('SP private key')
)
SAML2_SP_CERT_CONTENT = serializers.CharField(
allow_blank=True, required=False,
write_only=True, label=_('SP Public Cert')
write_only=True, label=_('SP cert')
)
SAML2_RENAME_ATTRIBUTES = serializers.DictField(required=False, label=_('Rename attr'))
SAML2_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))

View File

@ -39,7 +39,8 @@ class UserOtpEnableStartView(AuthMixin, TemplateView):
try:
self.get_user_from_session()
except SessionEmptyError:
return redirect('authentication:login') + '?_=otp_enable_start'
url = reverse('authentication:login') + '?_=otp_enable_start'
return redirect(url)
return super().get(request, *args, **kwargs)
@ -72,8 +73,8 @@ class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
def _pre_check_can_bind(self):
try:
user = self.get_user_from_session()
except:
verify_url = reverse('authentication:user-otp-enable-start')
except Exception as e:
verify_url = reverse('authentication:user-otp-enable-start') + f'?e={e}'
return HttpResponseRedirect(verify_url)
if user.otp_secret_key:

117
jms
View File

@ -2,20 +2,27 @@
# coding: utf-8
import os
import subprocess
import logging
import logging.handlers
import time
import argparse
import sys
import django
from django.core import management
from django.db.utils import OperationalError
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
APP_DIR = os.path.join(BASE_DIR, 'apps')
os.chdir(APP_DIR)
sys.path.insert(0, APP_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
django.setup()
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
try:
from apps.jumpserver import const
from jumpserver import const
__version__ = const.VERSION
except ImportError as e:
print("Not found __version__: {}".format(e))
@ -25,7 +32,7 @@ except ImportError as e:
sys.exit(1)
try:
from apps.jumpserver.const import CONFIG
from jumpserver.const import CONFIG
except ImportError as e:
print("Import error: {}".format(e))
print("Could not find config file, `cp config_example.yml config.yml`")
@ -48,56 +55,45 @@ except:
def check_database_connection():
os.chdir(os.path.join(BASE_DIR, 'apps'))
for i in range(60):
logging.info("Check database connection ...")
_code = subprocess.call("python manage.py showmigrations users ", shell=True)
if _code == 0:
logging.info(f"Check database connection: {i}")
try:
management.call_command('check', '--database', 'default')
logging.info("Database connect success")
return
except OperationalError:
logging.info('Database not setup, retry')
except Exception as e:
logging.error('Unexpect error occur: {}'.format(str(e)))
time.sleep(1)
logging.error("Connection database failed, exit")
sys.exit(10)
def check_migrations():
_apps_dir = os.path.join(BASE_DIR, 'apps')
_cmd = "python manage.py showmigrations | grep '\[.\]' | grep -v '\[X\]'"
_code = subprocess.call(_cmd, shell=True, cwd=_apps_dir)
if _code == 1:
return
# for i in range(3):
# print("!!! Warning: Has SQL migrations not perform, 有 SQL 变更没有执行")
# print("You should run `./PROC upgrade_db` first, 请先运行 ./PROC upgrade_db, 进行表结构变更")
# sys.exit(1)
def expire_caches():
_apps_dir = os.path.join(BASE_DIR, 'apps')
_code = subprocess.call("python manage.py expire_caches", shell=True, cwd=_apps_dir)
if _code == 1:
return
try:
management.call_command('expire_caches')
except:
pass
def perform_db_migrate():
logging.info("Check database structure change ...")
os.chdir(os.path.join(BASE_DIR, 'apps'))
logging.info("Migrate model change to database ...")
_code = subprocess.call('python3 manage.py migrate', shell=True)
if _code == 0:
return
logging.error('Perform migrate failed, exit')
sys.exit(11)
try:
management.call_command('migrate')
except Exception:
logging.error('Perform migrate failed, exit')
sys.exit(11)
def collect_static():
logging.info("Collect static files")
os.chdir(os.path.join(BASE_DIR, 'apps'))
_cmd = 'python3 manage.py collectstatic --no-input -c &> /dev/null '
subprocess.call(_cmd, shell=True)
logging.info("Collect static files done")
try:
management.call_command('collectstatic', '--no-input', '-c', verbosity=0, interactive=False)
logging.info("Collect static files done")
except:
pass
def compile_i81n_file():
@ -105,8 +101,7 @@ def compile_i81n_file():
if os.path.exists(django_mo_file):
return
os.chdir(os.path.join(BASE_DIR, 'apps'))
_cmd = 'python3 manage.py compilemessages --no-input -c &> /dev/null '
subprocess.call(_cmd, shell=True)
management.call_command('compilemessages', verbosity=0, interactive=False)
logging.info("Compile i18n files done")
@ -116,13 +111,34 @@ def upgrade_db():
def prepare():
# installer(check) & k8s(no check)
check_database_connection()
check_migrations()
upgrade_db()
expire_caches()
def start_services():
services = args.services if isinstance(args.services, list) else [args.services]
if action == 'start' and {'all', 'web'} & set(services):
prepare()
start_args = []
if args.daemon:
start_args.append('--daemon')
if args.worker:
start_args.extend(['--worker', str(args.worker)])
if args.force:
start_args.append('--force')
try:
management.call_command(action, *services, *start_args)
except KeyboardInterrupt:
logging.info('Cancel ...')
time.sleep(2)
except Exception as e:
logging.error("Start service error {}: {}".format(services, e))
time.sleep(2)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="""
@ -155,23 +171,4 @@ if __name__ == '__main__':
elif action == "collect_static":
collect_static()
else:
services = args.services if isinstance(args.services, list) else [args.services]
if action == 'start' and {'all', 'web'} & set(services):
prepare()
services_string = ' '.join(services)
cmd = f'python manage.py {args.action} {services_string}'
if args.daemon:
cmd += ' --daemon'
if args.worker:
cmd += f' --worker {args.worker}'
if args.force:
cmd += ' --force'
apps_dir = os.path.join(BASE_DIR, 'apps')
try:
# processes: main(3s) -> call(0.25s) -> service -> sub-process
code = subprocess.call(cmd, shell=True, cwd=apps_dir)
except KeyboardInterrupt:
time.sleep(2)
pass
start_services()