diff --git a/fir_admin/src/router/index.js b/fir_admin/src/router/index.js index 89acf09..69f9ae5 100644 --- a/fir_admin/src/router/index.js +++ b/fir_admin/src/router/index.js @@ -3,6 +3,8 @@ import Router from 'vue-router' Vue.use(Router) +const celery_flower = process.env.VUE_APP_BASE_API.split('/') + /* Layout */ import Layout from '@/layout' @@ -373,6 +375,16 @@ export const constantRoutes = [ } ] }, + { + path: 'celery-flower-link', + component: Layout, + children: [ + { + path: celery_flower[0] + '//' + celery_flower[2] + '/flower', + meta: { title: '任务监控', icon: 'link' } + } + ] + }, // 404 page must be placed at the end !!! { path: '*', redirect: '/404', hidden: true } diff --git a/fir_admin/src/store/modules/user.js b/fir_admin/src/store/modules/user.js index 2db5e2e..4a63d7e 100644 --- a/fir_admin/src/store/modules/user.js +++ b/fir_admin/src/store/modules/user.js @@ -1,7 +1,7 @@ import { login, logout, getInfo } from '@/api/user' import { getToken, setToken, removeToken } from '@/utils/auth' import { resetRouter } from '@/router' -const Base64 = require('js-base64').Base64; +const Base64 = require('js-base64').Base64 const getDefaultState = () => { return { diff --git a/fir_ser/admin/views/celery_flower.py b/fir_ser/admin/views/celery_flower.py new file mode 100644 index 0000000..6ef9a61 --- /dev/null +++ b/fir_ser/admin/views/celery_flower.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +from django.http import HttpResponse +from django.conf import settings +from django.utils.translation import ugettext as _ +from proxy.views import proxy_view +from rest_framework.views import APIView + +from api.utils.auth import AdminTokenAuthentication + +flower_url = f'{settings.CELERY_FLOWER_HOST}:{settings.CELERY_FLOWER_PORT}' + + +class CeleryFlowerView(APIView): + authentication_classes = [AdminTokenAuthentication, ] + + def get(self, request, path): + remote_url = 'http://{}/flower/{}'.format(flower_url, path) + try: + response = proxy_view(request, remote_url) + except Exception as e: + msg = _("

Flower service unavailable, check it

") + \ + '

{}
'.format(e) + response = HttpResponse(msg) + return response + + def post(self, request, path): + return self.get(request, path) diff --git a/fir_ser/api/management/__init__.py b/fir_ser/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fir_ser/api/management/commands/__init__.py b/fir_ser/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fir_ser/api/management/commands/expire_caches.py b/fir_ser/api/management/commands/expire_caches.py new file mode 100644 index 0000000..c69d090 --- /dev/null +++ b/fir_ser/api/management/commands/expire_caches.py @@ -0,0 +1,10 @@ +from django.core.management.base import BaseCommand + +from api.tasks import start_api_sever_do_clean + + +class Command(BaseCommand): + help = 'Expire caches' + + def handle(self, *args, **options): + start_api_sever_do_clean() diff --git a/fir_ser/api/management/commands/restart.py b/fir_ser/api/management/commands/restart.py new file mode 100644 index 0000000..57285f9 --- /dev/null +++ b/fir_ser/api/management/commands/restart.py @@ -0,0 +1,6 @@ +from .services.command import BaseActionCommand, Action + + +class Command(BaseActionCommand): + help = 'Restart services' + action = Action.restart.value diff --git a/fir_ser/api/management/commands/services/__init__.py b/fir_ser/api/management/commands/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fir_ser/api/management/commands/services/command.py b/fir_ser/api/management/commands/services/command.py new file mode 100644 index 0000000..2dd5c8c --- /dev/null +++ b/fir_ser/api/management/commands/services/command.py @@ -0,0 +1,146 @@ +from django.core.management.base import BaseCommand, CommandError +from django.db.models import TextChoices +from .utils import ServicesUtil +from .hands import * + + +class Services(TextChoices): + # gunicorn = 'gunicorn', 'gunicorn' + uwsgi = 'uwsgi', 'uwsgi' + celery = 'celery', 'celery' + beat = 'beat', 'beat' + flower = 'flower', 'flower' + task = 'task', 'task' + all = 'all', 'all' + + @classmethod + def get_service_object_class(cls, name): + from . import services + services_map = { + cls.flower: services.FlowerService, + cls.celery: services.CeleryDefaultService, + cls.beat: services.BeatService, + cls.uwsgi: services.UwsgiService + } + return services_map.get(name) + + @classmethod + def api_services(cls): + return [cls.uwsgi] + + @classmethod + def flower_services(cls): + return [cls.flower] + + @classmethod + def beat_services(cls): + return [cls.beat] + + @classmethod + def celery_services(cls): + return [cls.celery] + + @classmethod + def task_services(cls): + return cls.celery_services() + cls.beat_services() + cls.flower_services() + + @classmethod + def all_services(cls): + return cls.task_services() + cls.api_services() + + @classmethod + def export_services_values(cls): + return [cls.all.value, cls.uwsgi.value, cls.task.value, cls.celery.value, cls.flower.value, cls.beat.value] + + @classmethod + def get_service_objects(cls, service_names, **kwargs): + services = set() + for name in service_names: + method_name = f'{name}_services' + if hasattr(cls, method_name): + _services = getattr(cls, method_name)() + elif hasattr(cls, name): + _services = [getattr(cls, name)] + else: + continue + services.update(set(_services)) + + service_objects = [] + for s in services: + service_class = cls.get_service_object_class(s.value) + if not service_class: + continue + kwargs.update({ + 'name': s.value + }) + service_object = service_class(**kwargs) + service_objects.append(service_object) + return service_objects + + +class Action(TextChoices): + start = 'start', 'start' + status = 'status', 'status' + stop = 'stop', 'stop' + restart = 'restart', 'restart' + + +class BaseActionCommand(BaseCommand): + help = 'Service Base Command' + + action = None + util = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def add_arguments(self, parser): + parser.add_argument( + 'services', nargs='+', choices=Services.export_services_values(), help='Service', + ) + parser.add_argument('-d', '--daemon', nargs="?", const=True) + parser.add_argument('-up', '--uwsgi_processes', type=int, nargs="?", default=4) + parser.add_argument('-ut', '--uwsgi_threads', type=int, nargs="?", default=2) + parser.add_argument('-usm', '--uwsgi_socket_mode', nargs="?", const=True, + help='run to bind socket mode, default http mode, only uwsgi service') + parser.add_argument('-f', '--force', nargs="?", const=True) + parser.add_argument('-u', '--uid', nargs="?", default='root', type=str) + parser.add_argument('-g', '--gid', nargs="?", default='root', type=str) + + def initial_util(self, *args, **options): + service_names = options.get('services') + service_kwargs = { + 'uwsgi_processes': options.get('uwsgi_processes'), + 'uwsgi_threads': options.get('uwsgi_threads'), + 'uwsgi_socket_mode': options.get('uwsgi_socket_mode'), + 'uid': options.get('uid'), + 'gid': options.get('gid'), + } + services = Services.get_service_objects(service_names=service_names, **service_kwargs) + + kwargs = { + 'services': services, + 'run_daemon': options.get('daemon', False), + 'stop_daemon': self.action == Action.stop.value and Services.all.value in service_names, + 'force_stop': options.get('force') or False, + } + self.util = ServicesUtil(**kwargs) + + def handle(self, *args, **options): + self.initial_util(*args, **options) + assert self.action in Action.values, f'The action {self.action} is not in the optional list' + _handle = getattr(self, f'_handle_{self.action}', lambda: None) + _handle() + + def _handle_start(self): + self.util.start_and_watch() + os._exit(0) + + def _handle_stop(self): + self.util.stop() + + def _handle_restart(self): + self.util.restart() + + def _handle_status(self): + self.util.show_status() diff --git a/fir_ser/api/management/commands/services/hands.py b/fir_ser/api/management/commands/services/hands.py new file mode 100644 index 0000000..4611099 --- /dev/null +++ b/fir_ser/api/management/commands/services/hands.py @@ -0,0 +1,28 @@ +import os +import sys +import logging +from django.conf import settings + +from config import BASECONF + +try: + from apps.jumpserver import const + + __version__ = const.VERSION +except ImportError as e: + print("Not found __version__: {}".format(e)) + print("Python is: ") + logging.info(sys.executable) + __version__ = 'Unknown' + +SOCKET_HOST = BASECONF.SERVER_BIND_HOST or '127.0.0.1' +SOCKET_PORT = BASECONF.SERVER_LISTEN_PORT or 8080 + +CELERY_FLOWER_HOST = BASECONF.CELERY_FLOWER_HOST or '127.0.0.1' +CELERY_FLOWER_PORT = BASECONF.CELERY_FLOWER_PORT or 5555 + +DEBUG = BASECONF.DEBUG or False +BASE_DIR = settings.BASE_DIR +APPS_DIR = BASE_DIR +LOG_DIR = os.path.join(BASE_DIR, 'logs') +TMP_DIR = os.path.join(BASE_DIR, 'tmp') diff --git a/fir_ser/api/management/commands/services/services/__init__.py b/fir_ser/api/management/commands/services/services/__init__.py new file mode 100644 index 0000000..5918309 --- /dev/null +++ b/fir_ser/api/management/commands/services/services/__init__.py @@ -0,0 +1,5 @@ +from .beat import * +from .flower import * +from .gunicorn import * +from .celery_default import * +from .uwsgi import * diff --git a/fir_ser/api/management/commands/services/services/base.py b/fir_ser/api/management/commands/services/services/base.py new file mode 100644 index 0000000..526231f --- /dev/null +++ b/fir_ser/api/management/commands/services/services/base.py @@ -0,0 +1,233 @@ +import abc +import time +import shutil +import psutil +import datetime +import threading +import subprocess +import pwd +from ..hands import * + + +def get_user_id(uid): + if uid.isdigit(): + try: + return pwd.getpwuid(int(uid)).pw_uid + except: + ... + else: + try: + return pwd.getpwnam(uid).pw_uid + except: + ... + + +class BaseService(object): + + def __init__(self, **kwargs): + self.name = kwargs['name'] + self.uid = kwargs['uid'] + self.gid = kwargs['gid'] + self._process = None + self.STOP_TIMEOUT = 10 + self.max_retry = 0 + self.retry = 3 + self.LOG_KEEP_DAYS = 7 + self.EXIT_EVENT = threading.Event() + + @property + @abc.abstractmethod + def cmd(self): + return [] + + @property + @abc.abstractmethod + def cwd(self): + return '' + + @property + def is_running(self): + if self.pid == 0: + return False + try: + os.kill(self.pid, 0) + except (OSError, ProcessLookupError): + return False + else: + return True + + def show_status(self): + if self.is_running: + msg = f'{self.name} is running: {self.pid}.' + else: + msg = f'{self.name} is stopped.' + print(msg) + + # -- log -- + @property + def log_filename(self): + return f'{self.name}.log' + + @property + def log_filepath(self): + return os.path.join(LOG_DIR, self.log_filename) + + @property + def log_file(self): + return open(self.log_filepath, 'a') + + @property + def log_dir(self): + return os.path.dirname(self.log_filepath) + + # -- end log -- + + # -- pid -- + @property + def pid_filepath(self): + return os.path.join(TMP_DIR, f'{self.name}.pid') + + @property + def pid(self): + if not os.path.isfile(self.pid_filepath): + return 0 + with open(self.pid_filepath) as f: + try: + pid = int(f.read().strip()) + except ValueError: + pid = 0 + return pid + + def write_pid(self): + with open(self.pid_filepath, 'w') as f: + f.write(str(self.process.pid)) + + def remove_pid(self): + if os.path.isfile(self.pid_filepath): + os.unlink(self.pid_filepath) + + # -- end pid -- + + # -- process -- + @property + def process(self): + if not self._process: + try: + self._process = psutil.Process(self.pid) + except: + pass + return self._process + + # -- end process -- + + # -- action -- + def open_subprocess(self): + self.set_work_dir_owner() + kwargs = {'cwd': self.cwd, 'stderr': self.log_file, 'stdout': self.log_file} + self._process = subprocess.Popen(self.cmd, **kwargs) + + def start(self): + if self.is_running: + self.show_status() + return + self.remove_pid() + self.open_subprocess() + self.write_pid() + self.start_other() + + def start_other(self): + pass + + def set_work_dir_owner(self): + uid = get_user_id(self.uid) + gid = get_user_id(self.gid) + u_gid = uid if uid else gid + if u_gid: + for (dir_path, dir_names, filenames) in os.walk(LOG_DIR): + for filename in filenames + dir_names: + os.chown(uid=u_gid, gid=u_gid, path=os.path.join(dir_path, filename)) + else: + logging.error(f'uid: {self.uid} gid:{self.gid} is not exists') + + def stop(self, force=False): + if not self.is_running: + self.show_status() + # self.remove_pid() + return + + print(f'Stop service: {self.name}', end='') + sig = 9 if force else 15 + os.kill(self.pid, sig) + + if self.process is None: + print("\033[31m No process found\033[0m") + return + try: + self.process.wait(5) + except: + pass + + for i in range(self.STOP_TIMEOUT): + if i == self.STOP_TIMEOUT - 1: + print("\033[31m Error\033[0m") + if not self.is_running: + print("\033[32m Ok\033[0m") + self.remove_pid() + break + else: + time.sleep(1) + continue + + def watch(self): + self._check() + if not self.is_running: + self._restart() + self._rotate_log() + + def _check(self): + now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"{now} Check service status: {self.name} -> ", end='') + if self.process: + try: + self.process.wait(1) # 不wait,子进程可能无法回收 + except: + pass + + if self.is_running: + print(f'running at {self.pid}') + else: + print(f'stopped at {self.pid}') + + def _restart(self): + if self.retry > self.max_retry: + logging.info(f"Service {self.name} start failed, exit") + self.EXIT_EVENT.set() + return + self.retry += 1 + logging.info(f'> Find {self.name} stopped, retry {self.retry}, {self.pid}') + self.start() + + def _rotate_log(self): + now = datetime.datetime.now() + _time = now.strftime('%H:%M') + if _time != '23:59': + return + + backup_date = now.strftime('%Y-%m-%d') + backup_log_dir = os.path.join(self.log_dir, backup_date) + if not os.path.exists(backup_log_dir): + os.mkdir(backup_log_dir) + + backup_log_path = os.path.join(backup_log_dir, self.log_filename) + if os.path.isfile(self.log_filepath) and not os.path.isfile(backup_log_path): + logging.info(f'Rotate log file: {self.log_filepath} => {backup_log_path}') + shutil.copy(self.log_filepath, backup_log_path) + with open(self.log_filepath, 'w') as f: + pass + + to_delete_date = now - datetime.timedelta(days=self.LOG_KEEP_DAYS) + to_delete_dir = os.path.join(LOG_DIR, to_delete_date.strftime('%Y-%m-%d')) + if os.path.exists(to_delete_dir): + logging.info(f'Remove old log: {to_delete_dir}') + shutil.rmtree(to_delete_dir, ignore_errors=True) + # -- end action -- diff --git a/fir_ser/api/management/commands/services/services/beat.py b/fir_ser/api/management/commands/services/services/beat.py new file mode 100644 index 0000000..0600f50 --- /dev/null +++ b/fir_ser/api/management/commands/services/services/beat.py @@ -0,0 +1,31 @@ +from ..hands import * +from .base import BaseService +from django.core.cache import cache + +__all__ = ['BeatService'] + + +class BeatService(BaseService): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.lock = cache.lock('beat-distribute-start-lock', timeout=60) + + @property + def cmd(self): + scheduler = "django_celery_beat.schedulers:DatabaseScheduler" + print("\n- Start Beat as Periodic Task Scheduler") + cmd = [ + 'celery', '-A', + 'fir_ser', 'beat', + '-l', 'INFO', + '--uid', self.uid, + '--gid', self.gid, + '--scheduler', scheduler, + '--max-interval', '60' + ] + return cmd + + @property + def cwd(self): + return APPS_DIR diff --git a/fir_ser/api/management/commands/services/services/celery_base.py b/fir_ser/api/management/commands/services/services/celery_base.py new file mode 100644 index 0000000..eca414a --- /dev/null +++ b/fir_ser/api/management/commands/services/services/celery_base.py @@ -0,0 +1,39 @@ +from ..hands import * +from .base import BaseService + + +class CeleryBaseService(BaseService): + + def __init__(self, queue, num=10, **kwargs): + super().__init__(**kwargs) + self.queue = queue + self.num = num + + @property + def cmd(self): + print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize())) + + os.environ.setdefault('PYTHONOPTIMIZE', '1') + os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True') + + if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + server_hostname = os.environ.get("SERVER_HOSTNAME") + if not server_hostname: + server_hostname = '%n' + + cmd = [ + 'celery', '-A', + 'fir_ser', 'worker', + '-l', 'INFO', + '--uid', self.uid, + '--gid', self.gid, + '-c', str(self.num), + '-Q', self.queue, + '-n', f'{self.queue}@{server_hostname}' + ] + return cmd + + @property + def cwd(self): + return APPS_DIR diff --git a/fir_ser/api/management/commands/services/services/celery_default.py b/fir_ser/api/management/commands/services/services/celery_default.py new file mode 100644 index 0000000..5d3e6d7 --- /dev/null +++ b/fir_ser/api/management/commands/services/services/celery_default.py @@ -0,0 +1,10 @@ +from .celery_base import CeleryBaseService + +__all__ = ['CeleryDefaultService'] + + +class CeleryDefaultService(CeleryBaseService): + + def __init__(self, **kwargs): + kwargs['queue'] = 'celery' + super().__init__(**kwargs) diff --git a/fir_ser/api/management/commands/services/services/flower.py b/fir_ser/api/management/commands/services/services/flower.py new file mode 100644 index 0000000..30ba8aa --- /dev/null +++ b/fir_ser/api/management/commands/services/services/flower.py @@ -0,0 +1,36 @@ +from ..hands import * +from .base import BaseService + +__all__ = ['FlowerService'] + + +class FlowerService(BaseService): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @property + def cmd(self): + print("\n- Start Flower as Task Monitor") + + if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + cmd = [ + 'celery', '-A', + 'fir_ser', 'flower', + '-l', 'INFO', + '--uid', self.uid, + '--gid', self.gid, + '--url_prefix=/flower', + '--auto_refresh=False', + '--max_tasks=1000', + f'--address={CELERY_FLOWER_HOST}', + f'--port={CELERY_FLOWER_PORT}', + # '--basic_auth=flower:ninevenxxx' + # '--tasks_columns=uuid,name,args,state,received,started,runtime,worker' + ] + return cmd + + @property + def cwd(self): + return APPS_DIR diff --git a/fir_ser/api/management/commands/services/services/gunicorn.py b/fir_ser/api/management/commands/services/services/gunicorn.py new file mode 100644 index 0000000..f3c3d76 --- /dev/null +++ b/fir_ser/api/management/commands/services/services/gunicorn.py @@ -0,0 +1,35 @@ +from ..hands import * +from .base import BaseService + +__all__ = ['GunicornService'] + + +class GunicornService(BaseService): + + def __init__(self, **kwargs): + self.worker = kwargs['worker_gunicorn'] + super().__init__(**kwargs) + + @property + def cmd(self): + print("\n- Start Gunicorn WSGI HTTP Server") + + log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' + bind = f'{HTTP_HOST}:{HTTP_PORT}' + cmd = [ + 'gunicorn', 'fir_ser.wsgi', + '-b', bind, + '-k', 'gthread', + '--threads', '10', + '-w', str(self.worker), + '--max-requests', '4096', + '--access-logformat', log_format, + '--access-logfile', '-' + ] + if DEBUG: + cmd.append('--reload') + return cmd + + @property + def cwd(self): + return APPS_DIR diff --git a/fir_ser/api/management/commands/services/services/uwsgi.py b/fir_ser/api/management/commands/services/services/uwsgi.py new file mode 100644 index 0000000..96466eb --- /dev/null +++ b/fir_ser/api/management/commands/services/services/uwsgi.py @@ -0,0 +1,45 @@ +from ..hands import * +from .base import BaseService + +__all__ = ['UwsgiService'] + + +class UwsgiService(BaseService): + + def __init__(self, **kwargs): + self.processes = kwargs['uwsgi_processes'] + self.threads = kwargs['uwsgi_threads'] + self.uwsgi_socket_mode = kwargs['uwsgi_socket_mode'] + super().__init__(**kwargs) + + @property + def cmd(self): + if os.getuid() == 0: + os.environ.setdefault('C_FORCE_ROOT', '1') + print("\n- Start Uwsgi WSGI HTTP Server") + bind = f'{SOCKET_HOST}:{SOCKET_PORT}' + cmd = [ + 'uwsgi', + '--uid', self.uid, + '--gid', self.gid, + '--processes', f'{self.processes}', + '--threads', f'{self.threads}', + '--wsgi-file', f"{BASE_DIR}/fir_ser/wsgi.py", + '--listen', '512', + '--chdir', BASE_DIR, + '--buffer-size', '65536', + '--vacuum', + '--enable-threads', + ] + + if self.uwsgi_socket_mode: + cmd.extend(['--socket', bind]) + else: + cmd.extend(['--http', bind]) + if DEBUG: + cmd.extend(['--touch-reload', BASE_DIR]) + return cmd + + @property + def cwd(self): + return APPS_DIR diff --git a/fir_ser/api/management/commands/services/utils.py b/fir_ser/api/management/commands/services/utils.py new file mode 100644 index 0000000..510929e --- /dev/null +++ b/fir_ser/api/management/commands/services/utils.py @@ -0,0 +1,141 @@ +import threading +import signal +import time +import daemon +from daemon import pidfile +from .hands import * +from .hands import __version__ +from .services.base import BaseService + + +class ServicesUtil(object): + + def __init__(self, services, run_daemon=False, force_stop=False, stop_daemon=False): + self._services = services + self.run_daemon = run_daemon + self.force_stop = force_stop + self.stop_daemon = stop_daemon + self.EXIT_EVENT = threading.Event() + self.check_interval = 30 + self.files_preserve_map = {} + + def restart(self): + self.stop() + time.sleep(5) + self.start_and_watch() + + def start_and_watch(self): + logging.info(time.ctime()) + logging.info(f'FlyApps version {__version__}, more see https://flyapps.cn') + self.start() + if self.run_daemon: + self.show_status() + with self.daemon_context: + self.watch() + else: + self.watch() + + def start(self): + for service in self._services: + service: BaseService + service.start() + self.files_preserve_map[service.name] = service.log_file + time.sleep(1) + + def stop(self): + for service in self._services: + service: BaseService + service.stop(force=self.force_stop) + + if self.stop_daemon: + self._stop_daemon() + + # -- watch -- + def watch(self): + while not self.EXIT_EVENT.is_set(): + try: + _exit = self._watch() + if _exit: + break + time.sleep(self.check_interval) + except KeyboardInterrupt: + print('Start stop services') + break + self.clean_up() + + def _watch(self): + for service in self._services: + service: BaseService + service.watch() + if service.EXIT_EVENT.is_set(): + self.EXIT_EVENT.set() + return True + return False + + # -- end watch -- + + def clean_up(self): + if not self.EXIT_EVENT.is_set(): + self.EXIT_EVENT.set() + + self.stop() + + def show_status(self): + for service in self._services: + service: BaseService + service.show_status() + + # -- daemon -- + def _stop_daemon(self): + if self.daemon_pid and self.daemon_is_running: + os.kill(self.daemon_pid, 15) + self.remove_daemon_pid() + + def remove_daemon_pid(self): + if os.path.isfile(self.daemon_pid_filepath): + os.unlink(self.daemon_pid_filepath) + + @property + def daemon_pid(self): + if not os.path.isfile(self.daemon_pid_filepath): + return 0 + with open(self.daemon_pid_filepath) as f: + try: + pid = int(f.read().strip()) + except ValueError: + pid = 0 + return pid + + @property + def daemon_is_running(self): + try: + os.kill(self.daemon_pid, 0) + except (OSError, ProcessLookupError): + return False + else: + return True + + @property + def daemon_pid_filepath(self): + return os.path.join(TMP_DIR, 'flyapps.pid') + + @property + def daemon_log_filepath(self): + return os.path.join(LOG_DIR, 'flyApps.log') + + @property + def daemon_context(self): + daemon_log_file = open(self.daemon_log_filepath, 'a') + context = daemon.DaemonContext( + pidfile=pidfile.TimeoutPIDLockFile(self.daemon_pid_filepath), + signal_map={ + signal.SIGTERM: lambda x, y: self.clean_up(), + signal.SIGHUP: 'terminate', + }, + stdout=daemon_log_file, + stderr=daemon_log_file, + files_preserve=list(self.files_preserve_map.values()), + detach_process=True, + ) + return context + # -- end daemon -- diff --git a/fir_ser/api/management/commands/start.py b/fir_ser/api/management/commands/start.py new file mode 100644 index 0000000..4c078a8 --- /dev/null +++ b/fir_ser/api/management/commands/start.py @@ -0,0 +1,6 @@ +from .services.command import BaseActionCommand, Action + + +class Command(BaseActionCommand): + help = 'Start services' + action = Action.start.value diff --git a/fir_ser/api/management/commands/status.py b/fir_ser/api/management/commands/status.py new file mode 100644 index 0000000..36f0d36 --- /dev/null +++ b/fir_ser/api/management/commands/status.py @@ -0,0 +1,6 @@ +from .services.command import BaseActionCommand, Action + + +class Command(BaseActionCommand): + help = 'Show services status' + action = Action.status.value diff --git a/fir_ser/api/management/commands/stop.py b/fir_ser/api/management/commands/stop.py new file mode 100644 index 0000000..a79a533 --- /dev/null +++ b/fir_ser/api/management/commands/stop.py @@ -0,0 +1,6 @@ +from .services.command import BaseActionCommand, Action + + +class Command(BaseActionCommand): + help = 'Stop services' + action = Action.stop.value diff --git a/fir_ser/api/tasks.py b/fir_ser/api/tasks.py index f3c80d9..41ab9b3 100644 --- a/fir_ser/api/tasks.py +++ b/fir_ser/api/tasks.py @@ -15,6 +15,7 @@ from api.utils.app.supersignutils import IosUtils, resign_by_app_id from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_ios_mobile_tmp_file, \ auto_check_ios_developer_active from api.utils.geetest.geetest_utils import check_bypass_status +from api.views.login import get_login_type from fir_ser.celery import app @@ -85,4 +86,5 @@ def auto_check_ios_developer_active_job(): @app.task def sync_wx_access_token_job(): - sync_wx_access_token() + if get_login_type().get('third', '').get('wxp'): + sync_wx_access_token() diff --git a/fir_ser/api/utils/auth.py b/fir_ser/api/utils/auth.py index 12001ca..7107488 100644 --- a/fir_ser/api/utils/auth.py +++ b/fir_ser/api/utils/auth.py @@ -9,9 +9,17 @@ from rest_framework.exceptions import AuthenticationFailed from api.models import UserInfo from rest_framework.permissions import BasePermission from fir_ser.settings import CACHE_KEY_TEMPLATE +from django.http.cookie import parse_cookie import base64 +def get_cookie_token(request): + cookies = request.META.get('HTTP_COOKIE') + if cookies: + cookie_dict = parse_cookie(cookies) + return cookie_dict.get('auth_token') + + def get_user_from_api_token(request): if request.method == "OPTIONS": return None @@ -29,7 +37,8 @@ def get_user_from_request_auth(request): if request.method == "OPTIONS": return None request_token = request.META.get("HTTP_AUTHORIZATION", - request.META.get("HTTP_X_TOKEN", request.query_params.get("token", None))) + request.META.get("HTTP_X_TOKEN", + request.query_params.get("token", get_cookie_token(request)))) if request_token: try: format_token = base64.b64decode(request_token).decode() diff --git a/fir_ser/api/utils/pay/util.py b/fir_ser/api/utils/pay/util.py index ce678a5..4b4fb01 100644 --- a/fir_ser/api/utils/pay/util.py +++ b/fir_ser/api/utils/pay/util.py @@ -4,13 +4,13 @@ # author: NinEveN # date: 2021/4/18 -from fir_ser.settings import PAY_CONFIG +from fir_ser.settings import PAY_CONFIG_KEY_INFO from api.utils.pay.ali import Alipay from api.utils.pay.wx import Weixinpay def get_pay_obj_form_name(pay_name): - for pay_info in PAY_CONFIG: + for pay_info in PAY_CONFIG_KEY_INFO: if pay_name == pay_info.get('NAME', '') and pay_info.get('ENABLED', False): auth_info = pay_info.get('AUTH', None) p_type = pay_info.get('TYPE', '') @@ -25,7 +25,7 @@ def get_pay_obj_form_name(pay_name): def get_enable_pay_choices(): pay_choices = [] - for pay_info in PAY_CONFIG: + for pay_info in PAY_CONFIG_KEY_INFO: if pay_info.get('ENABLED', False): pay_choices.append({'type': pay_info.get('TYPE'), 'name': pay_info.get('NAME', '')}) return pay_choices diff --git a/fir_ser/api/utils/sendmsg/sendmsg.py b/fir_ser/api/utils/sendmsg/sendmsg.py index 8762c75..30c05a0 100644 --- a/fir_ser/api/utils/sendmsg/sendmsg.py +++ b/fir_ser/api/utils/sendmsg/sendmsg.py @@ -7,7 +7,7 @@ from .aliyunApi import AliMsgSender from .emailApi import EmailMsgSender from .jiguangApi import JiGuangMsgSender -from fir_ser.settings import THIRD_PART_CONFIG +from fir_ser.settings import THIRD_PART_CONFIG_KEY_INFO import logging logger = logging.getLogger(__name__) @@ -20,7 +20,7 @@ def get_default_sender(send_type): 1: 阿里云 2: 极光 """ - sender_lists = THIRD_PART_CONFIG.get('sender') + sender_lists = THIRD_PART_CONFIG_KEY_INFO.get('sender_key') for sender in sender_lists: if sender.get("active", None): sender_type = sender.get('type', None) diff --git a/fir_ser/api/utils/storage/storage.py b/fir_ser/api/utils/storage/storage.py index 587d457..3df793f 100644 --- a/fir_ser/api/utils/storage/storage.py +++ b/fir_ser/api/utils/storage/storage.py @@ -10,7 +10,7 @@ from .qiniuApi import QiNiuOss from .localApi import LocalStorage import json, time, base64 from api.utils.baseutils import get_dict_from_filter_fields -from fir_ser.settings import THIRD_PART_CONFIG, CACHE_KEY_TEMPLATE +from fir_ser.settings import THIRD_PART_CONFIG_KEY_INFO, CACHE_KEY_TEMPLATE from django.core.cache import cache import logging @@ -146,7 +146,7 @@ class Storage(object): def get_local_storage(clean_cache=False): - storage_lists = THIRD_PART_CONFIG.get('storage') + storage_lists = THIRD_PART_CONFIG_KEY_INFO.get('storage_key') for storage in storage_lists: storage_type = storage.get('type', None) if storage_type == 0: @@ -169,7 +169,7 @@ def get_local_storage(clean_cache=False): def get_storage_form_conf(user): - storage_lists = THIRD_PART_CONFIG.get('storage', []) + storage_lists = THIRD_PART_CONFIG_KEY_INFO.get('storage_key', []) for storage in storage_lists: if storage.get("active", None): storage_type = storage.get('type', None) diff --git a/fir_ser/api/views/thirdlogin.py b/fir_ser/api/views/thirdlogin.py index 88472e9..81eba98 100644 --- a/fir_ser/api/views/thirdlogin.py +++ b/fir_ser/api/views/thirdlogin.py @@ -138,13 +138,11 @@ class ValidWxChatToken(APIView): if rec_msg.Eventkey == 'query_bind': if wx_user_obj: user_obj = wx_user_obj.user_id - content = f'绑定用户 {user_obj.first_name}\n' + content = f'绑定用户 {user_obj.first_name} ' if user_obj.email: - content += f' 登录邮箱:{user_obj.email}\n' + content += f' 登录邮箱:{user_obj.email} ' if user_obj.mobile: content += f' 登录手机:{user_obj.mobile}' - if content.endswith('\n'): - content = content[0:-1] user_cert_obj = UserCertificationInfo.objects.filter(user_id=user_obj, status=1).first() if user_cert_obj: @@ -202,12 +200,13 @@ class ValidWxChatToken(APIView): content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定' WxTemplateMsg().bind_failed_msg(to_user, wx_user_obj.nickname, content) else: - wx_user_info = update_or_create_wx_userinfo(to_user, user_obj) - content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功' - WxTemplateMsg().bind_success_msg(to_user, wx_user_info.get('nickname', ''), - user_obj.first_name) - - set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': user_obj.pk}) + if user_obj: + wx_user_info = update_or_create_wx_userinfo(to_user, user_obj) + content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功' + WxTemplateMsg().bind_success_msg(to_user, wx_user_info.get('nickname', ''), + user_obj.first_name) + if user_obj: + set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': user_obj.pk}) reply_msg = reply.TextMsg(to_user, from_user, content) result = reply_msg.send() else: diff --git a/fir_ser/config.py b/fir_ser/config.py index 47fd06f..e6bd355 100644 --- a/fir_ser/config.py +++ b/fir_ser/config.py @@ -17,7 +17,17 @@ class BASECONF(object): SECRET_KEY = 'j!g@^bc(z(a3*i&kp$_@bgb)bug&^#3=amch!3lz&1x&s6ss6t' ALLOWED_HOSTS = ['127.0.0.1', 'synchrotron', '172.16.133.34', 'ali.cdn.flyapp.dvcloud.xin', - 'api.src.flyapp.dvcloud.xin'] + 'api.src.flyapp.dvcloud.xin', 'app.hehelucky.cn'] + + # When Django start it will bind this host and port + # ./manage.py runserver 127.0.0.1:8080 + # uwsgi运行时绑定端口 + SERVER_BIND_HOST = '127.0.0.1' + SERVER_LISTEN_PORT = 8898 + + # celery flower 任务监控配置 + CELERY_FLOWER_PORT = 5566 + CELERY_FLOWER_HOST = '127.0.0.1' SERVER_DOMAIN = { 'IOS_PMFILE_DOWNLOAD_DOMAIN': { @@ -81,7 +91,7 @@ class THIRDLOGINCONF(object): class AUTHCONF(object): - # 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息 + # 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG_KEY_INFO.sender 信息 REGISTER = { "enable": True, "captcha": False, # 是否开启注册字母验证码 @@ -117,7 +127,7 @@ class AUTHCONF(object): } -class STORAGECONF(object): +class STORAGEKEYCONF(object): STORAGE = [ { 'name': 'local', @@ -247,7 +257,7 @@ class IPACONF(object): class PAYCONF(object): PAY_SUCCESS_URL = '%s/user/orders' % WEB_DOMAIN # 前端页面,支付成功跳转页面 - PAY_CONFIG = [ + PAY_CONFIG_KEY_INFO = [ { 'NAME': 'alipay', 'TYPE': 'ALI', diff --git a/fir_ser/fir_ser/settings.py b/fir_ser/fir_ser/settings.py index 488ed47..92135da 100644 --- a/fir_ser/fir_ser/settings.py +++ b/fir_ser/fir_ser/settings.py @@ -184,16 +184,16 @@ GEETEST_CYCLE_TIME = BASECONF.GEETEST_CYCLE_TIME GEETEST_BYPASS_STATUS_KEY = BASECONF.GEETEST_BYPASS_STATUS_KEY GEETEST_BYPASS_URL = BASECONF.GEETEST_BYPASS_URL -# 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息 +# 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG_KEY_INFO.sender 信息 REGISTER = AUTHCONF.REGISTER # 个人资料修改修改也会使用该配置 CHANGER = AUTHCONF.CHANGER LOGIN = AUTHCONF.LOGIN -THIRD_PART_CONFIG = { +THIRD_PART_CONFIG_KEY_INFO = { # APP存储配置 - 'storage': STORAGECONF.STORAGE, - 'sender': SENDERCONF.SENDER + 'storage_key': STORAGEKEYCONF.STORAGE, + 'sender_key': SENDERCONF.SENDER } CACHE_KEY_TEMPLATE = { 'user_can_download_key': 'user_can_download', @@ -256,7 +256,9 @@ SESSION_CACHE_ALIAS = "default" CAPTCHA_TIMEOUT = 5 # Minutes CAPTCHA_LENGTH = 6 # Chars -BASE_LOG_DIR = os.path.join(BASE_DIR, "logs") +BASE_LOG_DIR = os.path.join(BASE_DIR, "logs", "flyapp") +if not os.path.isdir(BASE_LOG_DIR): + os.makedirs(BASE_LOG_DIR) LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -325,7 +327,7 @@ LOGGING = { 'propagate': True, }, 'pay': { # 默认的logger应用如下配置 - 'handlers': ['pay'], # 上线之后可以把'console'移除 + 'handlers': ['pay'], 'level': 'DEBUG', 'propagate': True, }, @@ -333,7 +335,7 @@ LOGGING = { } PAY_SUCCESS_URL = PAYCONF.PAY_SUCCESS_URL -PAY_CONFIG = PAYCONF.PAY_CONFIG +PAY_CONFIG_KEY_INFO = PAYCONF.PAY_CONFIG_KEY_INFO # 结果存放到Django|redis # CELERY_RESULT_BACKEND = 'django-db' @@ -352,7 +354,7 @@ CELERY_BROKER_URL = 'redis://:%s@%s/2' % ( #: Only add pickle to this list if your broker is secured -CELERYD_CONCURRENCY = 4 # worker并发数 +CELERYD_CONCURRENCY = 10 # worker并发数 CELERYD_FORCE_EXECV = True # 非常重要,有些情况下可以防止死 CELERY_TASK_RESULT_EXPIRES = 3600 # 任务结果过期时间 @@ -415,3 +417,10 @@ MSGTEMPLATE = { 'ERROR_DEVELOPER': '用户 %s 你好,应用 %s 签名失败了,苹果开发者 %s 信息异常,请重新检查苹果开发者状态是否正常。感谢有你!', 'AUTO_CHECK_DEVELOPER': '用户 %s 你好,苹果开发者 %s 信息异常,请重新检查苹果开发者状态是否正常。感谢有你!', } + +## listen port + +SERVER_BIND_HOST = BASECONF.SERVER_BIND_HOST +SERVER_LISTEN_PORT = BASECONF.SERVER_LISTEN_PORT +CELERY_FLOWER_PORT = BASECONF.CELERY_FLOWER_PORT +CELERY_FLOWER_HOST = BASECONF.CELERY_FLOWER_HOST diff --git a/fir_ser/fir_ser/urls.py b/fir_ser/fir_ser/urls.py index 7823637..93bc4fb 100644 --- a/fir_ser/fir_ser/urls.py +++ b/fir_ser/fir_ser/urls.py @@ -16,6 +16,8 @@ Including another URLconf from django.contrib import admin from django.urls import re_path, include from django.views.static import serve + +from admin.views.celery_flower import CeleryFlowerView from fir_ser import settings from api.views.download import DownloadView, InstallView from api.views.receiveudids import IosUDIDView, ShowUdidView @@ -32,5 +34,5 @@ urlpatterns = [ re_path("install/(?P\w+)$", InstallView.as_view(), name="install"), re_path("^udid/(?P\w+)$", IosUDIDView.as_view()), re_path("^show_udid$", ShowUdidView.as_view()), - + re_path(r'flower/(?P.*)', CeleryFlowerView.as_view()), ] diff --git a/fir_ser/nginx-vhost.conf b/fir_ser/nginx-vhost.conf index b5bcbe1..5fbe7ca 100644 --- a/fir_ser/nginx-vhost.conf +++ b/fir_ser/nginx-vhost.conf @@ -1,9 +1,11 @@ upstream synchrotron { least_conn ; - #server 127.0.0.1:8899; - server 127.0.0.1:8898; - #server 127.0.0.1:8897; - #server 127.0.0.1:8896; + #hash $remote_addr consistent; + + server 127.0.0.1:8896 max_fails=1 fail_timeout=10s; + server 127.0.0.1:8897 max_fails=1 fail_timeout=10s; + server 127.0.0.1:8898 max_fails=1 fail_timeout=10s; + server 127.0.0.1:8899 backup; # 将服务器标记为备份服务器。当主服务器不可用时,将传递与备份服务器的连接 } proxy_set_header X-Forwarded-For $remote_addr; @@ -48,7 +50,7 @@ server uwsgi_connect_timeout 300; # 指定连接到后端uWSGI的超时时间。 uwsgi_read_timeout 300; # 指定接收uWSGI应答的超时时间,完成握手后接收uWSGI应答的超时时间。 include uwsgi_params; - uwsgi_pass 127.0.0.1:8899; + uwsgi_pass synchrotron; uwsgi_param UWSGI_SCRIPT wsgi; } diff --git a/fir_ser/requirements.txt b/fir_ser/requirements.txt index f2fe256..efe2f92 100644 --- a/fir_ser/requirements.txt +++ b/fir_ser/requirements.txt @@ -15,4 +15,8 @@ xmltodict==0.12.0 pyOpenSSL==20.0.1 paramiko==2.7.2 PyJWT==2.1.0 -drf-extensions==0.7.0 \ No newline at end of file +drf-extensions==0.7.0 +python-daemon==2.2.3 +psutil==5.6.6 +flower==1.0.0 +django-proxy==1.2.1 \ No newline at end of file