Browse Source

新增: 云飞监控封装权限验证与分页功能

root 4 years ago
parent
commit
f21b8db877
51 changed files with 1607 additions and 1 deletions
  1. 26 1
      README.md
  2. BIN
      monitor/db.sqlite3
  3. 21 0
      monitor/manage.py
  4. 0 0
      monitor/monitor/__init__.py
  5. 52 0
      monitor/monitor/exception.py
  6. 59 0
      monitor/monitor/middleware.py
  7. 27 0
      monitor/monitor/pagination.py
  8. 16 0
      monitor/monitor/permissions.py
  9. 133 0
      monitor/monitor/settings.py
  10. 32 0
      monitor/monitor/urls.py
  11. 16 0
      monitor/monitor/wsgi.py
  12. 0 0
      monitor/monitor_app/__init__.py
  13. 3 0
      monitor/monitor_app/admin.py
  14. 5 0
      monitor/monitor_app/apps.py
  15. 0 0
      monitor/monitor_app/migrations/__init__.py
  16. 3 0
      monitor/monitor_app/models.py
  17. 0 0
      monitor/monitor_app/process/__init__.py
  18. 57 0
      monitor/monitor_app/process/serializers.py
  19. 89 0
      monitor/monitor_app/process/views.py
  20. 0 0
      monitor/monitor_app/system/__init__.py
  21. 78 0
      monitor/monitor_app/system/serializers.py
  22. 15 0
      monitor/monitor_app/system/test.py
  23. 37 0
      monitor/monitor_app/system/views.py
  24. 0 0
      monitor/monitor_app/test/__init__.py
  25. 13 0
      monitor/monitor_app/test/serializers.py
  26. 30 0
      monitor/monitor_app/test/views.py
  27. 9 0
      monitor/monitor_app/tests.py
  28. 14 0
      monitor/monitor_app/urls.py
  29. 0 0
      monitor/monitor_app/user/__init__.py
  30. 22 0
      monitor/monitor_app/user/serializers.py
  31. 44 0
      monitor/monitor_app/user/views.py
  32. 3 0
      monitor/monitor_app/views.py
  33. 0 0
      monitor/supervisord/__init__.py
  34. 0 0
      monitor/supervisord/build_monitor/__init__.py
  35. 44 0
      monitor/supervisord/build_monitor/dingding_monitor.py
  36. 47 0
      monitor/supervisord/build_monitor/redis_monitor.py
  37. 55 0
      monitor/supervisord/build_monitor/system_monitor.py
  38. 4 0
      monitor/supervisord/config/demo_env/dingding.yml
  39. 15 0
      monitor/supervisord/config/demo_env/monitor.yml
  40. 7 0
      monitor/supervisord/config/demo_env/redis.yml
  41. 6 0
      monitor/supervisord/monitor_test/ceshi.py
  42. 4 0
      monitor/supervisord/monitor_test/test2.py
  43. 246 0
      monitor/supervisord/yf_supervisord.py
  44. 0 0
      monitor/utils/__init__.py
  45. 85 0
      monitor/utils/comm_tools.py
  46. 115 0
      monitor/utils/exception_warning.py
  47. 61 0
      monitor/utils/supervisord_utils.py
  48. 18 0
      monitor/yfmonitor_gunicorn.py
  49. 32 0
      requirements.txt
  50. 45 0
      restart.sh
  51. 19 0
      stop.sh

+ 26 - 1
README.md

@@ -1,3 +1,28 @@
 # yunfei_monitor
 # yunfei_monitor
 
 
-云飞服务器监控
+django 2.X 需要sqlite3 版本为3.8.x以上 
+
+更新sqlite3 
+
+wget https://www.sqlite.org/2022/sqlite-autoconf-3380100.tar.gz
+
+tar -xvzf sqlite-autoconf-3380100.tar.gz
+
+cd sqlite-autoconf-3380100/
+
+./configure --prefix=/usr/local
+
+make && make install
+
+mv /usr/bin/sqlite3  /usr/bin/sqlite3_old
+
+ln -s /usr/local/bin/sqlite3   /usr/bin/sqlite3
+
+echo "/usr/local/lib" > /etc/ld.so.conf.d/sqlite3.conf
+
+ldconfig
+
+sqlite3 --version
+
+云飞服务器监控
+

BIN
monitor/db.sqlite3


+ 21 - 0
monitor/manage.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'monitor.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()

+ 0 - 0
monitor/monitor/__init__.py


+ 52 - 0
monitor/monitor/exception.py

@@ -0,0 +1,52 @@
+# coding:utf-8
+
+# 自定义异常处理
+from rest_framework.views import exception_handler
+from rest_framework.views import Response
+from rest_framework import status
+
+from django.http.response import JsonResponse
+from rest_framework.views import exception_handler
+
+
+def custom_exception_handler(exc, context):
+    # Call REST framework's default exception handler first,
+    # to get the standard error response.
+    response = exception_handler(exc, context)
+    print('nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn')
+    # Now add the HTTP status code to the response.
+    if response is not None:
+        response.data['status_code'] = response.status_code
+
+    return response
+
+
+def globe_exception_handler(exc, context):
+    """
+        Below is the global exception handler of drf
+        Http404 / PermissionDenied / APIException
+    """
+
+    # Call REST framework's default exception handler
+    response = exception_handler(exc, context)
+    request = context['request']
+
+    # Now add the HTTP status code to the response.
+    if response is not None:
+        if isinstance(response.data, list):
+            msg = '; '.join(response.data)
+        elif isinstance(response.data, str):
+            msg = response.data
+        else:
+            msg = 'Sorry, we make a mistake (* ̄︶ ̄)!'
+
+        ex_data = {
+            "msg": msg,
+            "error_code": 1000,
+            "request": request.path
+        }
+        return JsonResponse(data=ex_data, status=response.status_code)
+
+    # 如果 response 为 None 则直接触发上面的 ExceptionGlobeMiddleware
+    return response
+

+ 59 - 0
monitor/monitor/middleware.py

@@ -0,0 +1,59 @@
+# coding:utf-8
+
+from django.http.response import JsonResponse
+from django.utils.deprecation import MiddlewareMixin
+
+
+class ExceptionGlobeMiddleware(MiddlewareMixin):
+    """
+        Below is the global exception handler of django
+    """
+    def process_response(self, request, response):
+        '''视图函数调用之后,response返回浏览器之前'''
+        status_code = response.status_code
+        path = request.path
+        if path.startswith('/api/schema'):
+            return response
+
+        result_data = {
+            'msg': '',
+            'code': 400,
+            'result': {
+                'data': {},
+                'paging': {}
+            }
+        }
+        if status_code in [200, 400, 403]:
+            if status_code == 200:
+                data = response.data
+                if data and 'paging' in data:
+                    result_data['result'] = data
+                else:
+                    result_data['result']['data'] = data
+                result_data['code'] = 0
+            elif status_code == 400:
+                data = response.data
+                if isinstance(data, dict):
+                    try:
+                        for value in data.values():
+                            result_data['msg'] = str(value[0])
+                            break
+                    except Exception as e:
+                        pass
+            elif status_code == 403:
+                result_data['msg'] = "无权限访问,请先登陆"
+                result_data['code'] = 403
+            response = JsonResponse(data=result_data, status=status_code)
+        return response
+
+    def process_exception(self, request, exception):
+        # 捕获其他异常,直接返回 500
+        ex_data = {
+            "msg": str(exception),
+            "code": 500,
+            'result': {
+                'data': {},
+                'paging': {}
+            }
+        }
+        return JsonResponse(data=ex_data, status=500)

+ 27 - 0
monitor/monitor/pagination.py

@@ -0,0 +1,27 @@
+# coding:utf-8
+
+from rest_framework.response import Response
+from rest_framework.pagination import PageNumberPagination
+
+
+class CustomPagination(PageNumberPagination):
+    page_size = 2
+    max_page_size = 20
+    page_size_query_param = 'size'
+    page_query_param = 'page'
+
+    def get_paginated_response(self, data):
+        next = self.page.next_page_number() if self.page.has_next() else 1
+        previous = self.page.previous_page_number() if self.page.has_previous() else 1
+
+        return Response({
+            'paging': {
+                'next': next,
+                'previous': previous,
+                'total': self.page.paginator.count,
+                'page': self.page.start_index(),
+                'page_size': self.page.paginator.per_page,
+                'total_page': self.page.paginator.num_pages
+            },
+            'data': data
+        })

+ 16 - 0
monitor/monitor/permissions.py

@@ -0,0 +1,16 @@
+# coding:utf-8
+
+from django.contrib.auth.models import User
+from rest_framework.permissions import BasePermission, IsAuthenticated
+
+
+class LoginPermission(BasePermission):
+
+    def has_permission(self, request, view):
+        status = True
+        try:
+            user_id = request.session['user_id']
+            User.objects.get(id=user_id)
+        except Exception as e:
+            status = False
+        return status

+ 133 - 0
monitor/monitor/settings.py

@@ -0,0 +1,133 @@
+"""
+Django settings for monitor project.
+
+Generated by 'django-admin startproject' using Django 2.2.27.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'c2pfrk2se(9slc%1o=+k)2ukd9q5$8fqwq4cysu*o-$j=nzvd*'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+APPEND_SLASH = False
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+    'drf_spectacular',
+    'monitor_app'
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'monitor.middleware.ExceptionGlobeMiddleware'
+]
+
+REST_FRAMEWORK = {
+    # YOUR SETTINGS
+    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
+    # 全局配置异常模块
+    # 'EXCEPTION_HANDLER': 'monitor.exception.globe_exception_handler',
+    'DEFAULT_PAGINATION_CLASS': 'monitor.pagination.CustomPagination',
+}
+
+ROOT_URLCONF = 'monitor.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'monitor.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'

+ 32 - 0
monitor/monitor/urls.py

@@ -0,0 +1,32 @@
+"""monitor URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
+
+
+api_urlpatterns = [
+    path('', include('monitor_app.urls'))
+]
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('api-auth/', include('rest_framework.urls')),
+    path('api/', include(api_urlpatterns)),
+    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
+    path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
+    path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
+]

+ 16 - 0
monitor/monitor/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for monitor project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'monitor.settings')
+
+application = get_wsgi_application()

+ 0 - 0
monitor/monitor_app/__init__.py


+ 3 - 0
monitor/monitor_app/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 5 - 0
monitor/monitor_app/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class MonitorAppConfig(AppConfig):
+    name = 'monitor_app'

+ 0 - 0
monitor/monitor_app/migrations/__init__.py


+ 3 - 0
monitor/monitor_app/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 0 - 0
monitor/monitor_app/process/__init__.py


+ 57 - 0
monitor/monitor_app/process/serializers.py

@@ -0,0 +1,57 @@
+# coding:utf-8
+
+import datetime
+from rest_framework import serializers
+
+from utils.supervisord_utils import SupervisordUtils
+from drf_spectacular.utils import extend_schema_field
+from drf_spectacular.types import OpenApiTypes
+
+
+class ProcessListSerializer(serializers.Serializer):
+    pid = serializers.IntegerField(help_text='进程id', default=1)
+    name = serializers.CharField(help_text="进程名称", default="nginx")
+    start_time = serializers.DateTimeField(help_text="启动时间", default="2020-03-06 20:54:00")
+    state_name = serializers.CharField(label="运行状态", help_text="当运行状态不等于RUNNING时候,停止按钮设置为不可点击状态", default="RUNNING")
+    description = serializers.CharField(help_text="描述信息", default="Exited too quickly (process log may have details)")
+
+    def to_representation(self, instance):
+        start_date = datetime.datetime.fromtimestamp(instance['start'])
+        start_time = start_date.strftime('%Y-%m-%d %H:%M:%S')
+        state_name = instance['statename']
+        description = instance['description']
+        pid = -1
+        if state_name == 'RUNNING':
+            pid = instance['pid']
+            now = datetime.datetime.fromtimestamp(instance['now'])
+            diff_date = now - start_date
+            description = '已运行 ' + str(diff_date).replace('days', '天')
+
+        return {
+            "pid": pid,
+            "name": instance['name'],
+            "start_time": start_time,
+            "state_name": instance['statename'],
+            "description": description
+        }
+
+
+class ProcessDetailDeleteSerializer(serializers.Serializer):
+    name = serializers.CharField(required=True, help_text="进程名称", error_messages={
+        'required': '参数必须存在',
+        'blank': '进程名称不能为空',
+    })
+
+    def validate_name(self, value):
+        if not SupervisordUtils().is_exists(value):
+            raise serializers.ValidationError('{}进程不存在'.format(value))
+
+
+class ProcessDetailPutSerializer(ProcessDetailDeleteSerializer):
+    action = serializers.CharField(label="操作方式", required=True, help_text="stop 表示停止,restart 表示重启")
+
+    def validate_action(self, value):
+        if value not in ['stop', 'restart']:
+            raise serializers.ValidationError('仅支持stop或restart')
+
+

+ 89 - 0
monitor/monitor_app/process/views.py

@@ -0,0 +1,89 @@
+# coding:utf-8
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiParameter, OpenApiExample, OpenApiTypes
+from .serializers import ProcessListSerializer, ProcessDetailPutSerializer, ProcessDetailDeleteSerializer
+from monitor.pagination import CustomPagination
+from utils.supervisord_utils import SupervisordUtils
+from monitor.permissions import LoginPermission
+
+
+class ProcessListView(APIView):
+    permission_classes = [LoginPermission]
+
+    @extend_schema(
+        parameters=[
+            OpenApiParameter(
+                name='page',
+                type=OpenApiTypes.STR,
+                location=OpenApiParameter.QUERY,
+                description='页码,第几页',
+            ),
+            OpenApiParameter(
+                name='size',
+                type=OpenApiTypes.STR,
+                location=OpenApiParameter.QUERY,
+                description='每页条目数',
+            ),
+            OpenApiParameter(
+                name='name',
+                type=OpenApiTypes.STR,
+                location=OpenApiParameter.QUERY,
+                description='进程名称',
+            ),
+        ],
+        description="获取监控列表接口",
+        request=None,
+        responses=ProcessListSerializer
+    )
+    def get(self, request):
+        name = request.query_params.get('name', '').strip()
+        data_list = SupervisordUtils().list(name)
+        page = CustomPagination()
+        queryset = page.paginate_queryset(data_list, request=request, view=self)
+        serobj = ProcessListSerializer(queryset, many=True)
+        return page.get_paginated_response(serobj.data)
+
+
+class ProcessDetailView(APIView):
+    permission_classes = [LoginPermission]
+
+    @extend_schema(
+        description="停止/重启进程接口",
+        request=ProcessDetailPutSerializer,
+
+        responses={
+            200: OpenApiResponse(description="无返回值")
+        }
+    )
+    def put(self, request):
+        serobj = ProcessDetailPutSerializer(data=request.data)
+        if not serobj.is_valid():
+            return Response(serobj.errors, status=400)
+
+        action = request.data['action']
+        name = request.data['name']
+        sup = SupervisordUtils()
+        if action == 'restart':
+            sup.restart(name)
+        else:
+            sup.stop(name)
+        return Response(status=200)
+
+    @extend_schema(
+        description="删除进程进程接口",
+        request=ProcessDetailDeleteSerializer,
+        responses={
+            200: OpenApiResponse(description="无返回值")
+        }
+    )
+    def delete(self, request):
+        serobj = ProcessDetailDeleteSerializer(data=request.data)
+        if not serobj.is_valid():
+            return Response(serobj.errors, status=400)
+
+        name = request.data['name']
+        sup = SupervisordUtils()
+        sup.remove(name)
+        return Response(status=200)

+ 0 - 0
monitor/monitor_app/system/__init__.py


+ 78 - 0
monitor/monitor_app/system/serializers.py

@@ -0,0 +1,78 @@
+# coding:utf-8
+
+import datetime
+from rest_framework import serializers
+
+from utils.supervisord_utils import SupervisordUtils
+from drf_spectacular.utils import extend_schema_field
+from drf_spectacular.types import OpenApiTypes
+
+
+class SystemDetailSerializer(serializers.Serializer):
+    cpu_rate = serializers.IntegerField(help_text='cpu使用率', default=0.0)
+    disk_rate = serializers.IntegerField(help_text='磁盘使用率', default=0.0)
+    memory_rate = serializers.IntegerField(help_text='内存使用率', default=0.0)
+    disk_used = serializers.CharField(help_text="磁盘已使用", default="20G")
+    disk_total = serializers.CharField(help_text=" 磁盘总数", default="100G")
+    memory_used = serializers.CharField(help_text="内存已使用", default="3G")
+    memory_total = serializers.CharField(help_text="内存总数", default="8G")
+
+    def to_representation(self, instance):
+        return {
+            'cpu_rate': instance['cpu_rate'],
+            'disk_rate': instance['disk_rate'],
+            'memory_rate': instance['memory_rate'],
+            'disk_used': instance['disk_used'],
+            'disk_total': instance['disk_total'],
+            'memory_used': instance['memory_used'],
+            'memory_total': instance['memory_total']
+        }
+
+
+class ProcessListSerializer(serializers.Serializer):
+    pid = serializers.IntegerField(help_text='进程id', default=1)
+    name = serializers.CharField(help_text="进程名称", default="nginx")
+    start_time = serializers.DateTimeField(help_text="启动时间", default="2020-03-06 20:54:00")
+    state_name = serializers.CharField(label="运行状态", help_text="当运行状态不等于RUNNING时候,停止按钮设置为不可点击状态", default="RUNNING")
+    description = serializers.CharField(help_text="描述信息", default="Exited too quickly (process log may have details)")
+
+    def to_representation(self, instance):
+        start_date = datetime.datetime.fromtimestamp(instance['start'])
+        start_time = start_date.strftime('%Y-%m-%d %H:%M:%S')
+        state_name = instance['statename']
+        description = instance['description']
+        pid = -1
+        if state_name == 'RUNNING':
+            pid = instance['pid']
+            now = datetime.datetime.fromtimestamp(instance['now'])
+            diff_date = now - start_date
+            description = '已运行 ' + str(diff_date).replace('days', '天')
+
+        return {
+            "pid": pid,
+            "name": instance['name'],
+            "start_time": start_time,
+            "state_name": instance['statename'],
+            "description": description
+        }
+
+
+class ProcessDetailDeleteSerializer(serializers.Serializer):
+    name = serializers.CharField(required=True, help_text="进程名称", error_messages={
+        'required': '参数必须存在',
+        'blank': '进程名称不能为空',
+    })
+
+    def validate_name(self, value):
+        if not SupervisordUtils().is_exists(value):
+            raise serializers.ValidationError('{}进程不存在'.format(value))
+
+
+class ProcessDetailPutSerializer(ProcessDetailDeleteSerializer):
+    action = serializers.CharField(label="操作方式", required=True, help_text="stop 表示停止,restart 表示重启")
+
+    def validate_action(self, value):
+        if value not in ['stop', 'restart']:
+            raise serializers.ValidationError('仅支持stop或restart')
+
+

+ 15 - 0
monitor/monitor_app/system/test.py

@@ -0,0 +1,15 @@
+# coding:utf-8
+
+import psutil
+from psutil._common import bytes2human
+
+
+print(psutil.cpu_percent(interval=0.2))
+print(psutil.virtual_memory())
+memory = psutil.virtual_memory()
+unit = 1024 * 1024
+
+print(bytes2human(memory.total), bytes2human(memory.available), bytes2human(memory.used), bytes2human(memory.free), bytes2human(memory.active))
+
+d = psutil.disk_usage('/')
+print(d)

+ 37 - 0
monitor/monitor_app/system/views.py

@@ -0,0 +1,37 @@
+# coding:utf-8
+
+import psutil
+from psutil._common import bytes2human
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from drf_spectacular.utils import extend_schema, OpenApiResponse
+from .serializers import SystemDetailSerializer
+from monitor.permissions import LoginPermission
+
+
+class SystemDetailView(APIView):
+    permission_classes = [LoginPermission]
+
+    @extend_schema(
+        description="获取系统信息功能接口",
+        responses={
+            200: SystemDetailSerializer,
+            400: OpenApiResponse(description="错误状态码")
+        }
+    )
+    def get(self, request):
+        cpu_rate = psutil.cpu_percent(interval=0.2)
+        memory = psutil.virtual_memory()
+        disk = psutil.disk_usage('/')
+
+        data = {
+            'cpu_rate': cpu_rate,
+            'disk_rate': disk.percent,
+            'memory_rate': memory.percent,
+            'disk_used': bytes2human(disk.used),
+            'disk_total': bytes2human(disk.total),
+            'memory_used': bytes2human(memory.used),
+            'memory_total': bytes2human(memory.total)
+        }
+        serobj = SystemDetailSerializer(data)
+        return Response(data=serobj.data, status=200)

+ 0 - 0
monitor/monitor_app/test/__init__.py


+ 13 - 0
monitor/monitor_app/test/serializers.py

@@ -0,0 +1,13 @@
+# coding:utf-8
+
+from rest_framework import serializers
+
+
+class ListProcessSerializers(serializers.Serializer):
+    pid = serializers.IntegerField()
+    name = serializers.CharField(max_length=256)
+    start_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
+    state_name = serializers.CharField(max_length=10)
+    description = serializers.CharField(max_length=50)
+
+

+ 30 - 0
monitor/monitor_app/test/views.py

@@ -0,0 +1,30 @@
+# coding:utf-8
+
+import datetime
+from django.http.response import HttpResponse
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from .serializers import ListProcessSerializers
+
+
+class ListProcessView(APIView):
+    def get(self, request):
+        data_list = [
+            {
+                'pid': 1,
+                'name': '123',
+                'start_time': datetime.datetime.now(),
+                'state_name': 'run',
+                'description': 'xxxxxx'
+
+            }
+        ]
+
+        serobj = ListProcessSerializers(data_list, many=True)
+        print(serobj)
+        print(serobj.data)
+
+        return Response(serobj.data)
+
+

+ 9 - 0
monitor/monitor_app/tests.py

@@ -0,0 +1,9 @@
+from django.test import TestCase
+
+# Create your tests here.
+
+
+cmd = 'at > /xxx/python /root/project/test.py'
+
+import sys
+sys.exit(0)

+ 14 - 0
monitor/monitor_app/urls.py

@@ -0,0 +1,14 @@
+# coding:utf-8
+
+from django.urls import path, re_path
+from .process.views import ProcessListView, ProcessDetailView
+from .system.views import SystemDetailView
+from .user.views import UserLoginView, UserLogoutView
+
+urlpatterns = [
+    re_path('user/login/?', UserLoginView.as_view()),
+    re_path('user/logout/?', UserLogoutView.as_view()),
+    re_path('process/?', ProcessListView.as_view()),
+    re_path('process/detail/?', ProcessDetailView.as_view()),
+    re_path('system/detail/?', SystemDetailView.as_view())
+]

+ 0 - 0
monitor/monitor_app/user/__init__.py


+ 22 - 0
monitor/monitor_app/user/serializers.py

@@ -0,0 +1,22 @@
+# coding:utf-8
+
+from django.contrib.auth import authenticate
+from django.contrib.auth.models import User
+from rest_framework import serializers
+
+
+class UserDetailSerializers(serializers.Serializer):
+    username = serializers.CharField(max_length=64, help_text="用户名称", required=True, error_messages={
+        'required': '参数必须存在',
+        'blank': '用户名不能为空',
+    })
+    password = serializers.CharField(max_length=64, help_text="用户密码", required=True, error_messages={
+        'required': '参数必须存在',
+        'blank': '密码不能为空',
+    })
+
+    def validate(self, data):
+        username, password = data.get('username'), data.get('password')
+        if not authenticate(username=username, password=password):
+            raise serializers.ValidationError('用户名或密码错误')
+        return data

+ 44 - 0
monitor/monitor_app/user/views.py

@@ -0,0 +1,44 @@
+# coding:utf-8
+
+
+from django.contrib.auth import authenticate
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from drf_spectacular.utils import extend_schema, OpenApiResponse
+from .serializers import UserDetailSerializers
+from monitor.permissions import LoginPermission
+
+
+class UserLoginView(APIView):
+    @extend_schema(
+        description="用户登陆接口",
+        request=UserDetailSerializers,
+
+        responses={
+            200: OpenApiResponse(description="无返回值")
+        }
+    )
+    def post(self, request):
+        serobj = UserDetailSerializers(data=request.data)
+        if not serobj.is_valid():
+            return Response(serobj.errors, status=400)
+        username, password = request.data['username'], request.data['password']
+        user = authenticate(username=username, password=password)
+        request.session['user_id'] = user.id
+        request.session.save()
+        return Response(status=200)
+
+
+class UserLogoutView(APIView):
+    permission_classes = [LoginPermission]
+
+    @extend_schema(
+        description="用户退出接口",
+        responses={
+            200: OpenApiResponse(description="无返回值")
+        }
+    )
+    def get(self, request):
+        del request.session['user_id']
+        request.session.save()
+        return Response(status=200)

+ 3 - 0
monitor/monitor_app/views.py

@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.

+ 0 - 0
monitor/supervisord/__init__.py


+ 0 - 0
monitor/supervisord/build_monitor/__init__.py


+ 44 - 0
monitor/supervisord/build_monitor/dingding_monitor.py

@@ -0,0 +1,44 @@
+# coding:utf-8
+
+'''自动推送消息到钉钉'''
+
+import os
+import sys
+import json
+import time
+
+from dingtalkchatbot.chatbot import DingtalkChatbot
+
+local_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if local_path not in sys.path:
+    sys.path.append(local_path)
+
+from monitor.utils.comm_tools import get_redis_connection, get_dingding_config
+
+
+class DingDingUtils:
+    def __init__(self):
+        # 钉钉机器人地址和密钥
+        ding_config = get_dingding_config()
+        webhook = ding_config['webhook']
+        secret = ding_config['secret']
+        self.ding = DingtalkChatbot(webhook=webhook, secret=secret)
+
+    def run(self):
+        con = get_redis_connection()
+        while True:
+            v = con.blpop('yf_dingding')[1]
+            data = json.loads(v)
+            content, monitor_list = data['content'], data.get('monitor_list', [])
+            for k in monitor_list:
+                m = con.hget('yf_sys_monitor', k)
+                if m:
+                    content += '\n\n{}'.format(m)
+            self.ding.send_text(content)
+            time.sleep(0.6)
+
+
+if __name__ == '__main__':
+    DingDingUtils().run()
+
+

+ 47 - 0
monitor/supervisord/build_monitor/redis_monitor.py

@@ -0,0 +1,47 @@
+# coding:utf-8
+
+import os
+import sys
+import psutil
+import subprocess
+
+local_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if local_path not in sys.path:
+    sys.path.append(local_path)
+
+from monitor.utils.comm_tools import shell_cmd
+
+
+def _has_redis():
+    is_redis = False
+    for proc in psutil.process_iter(['pid', 'name']):
+        if proc.name() == 'redis-server':
+            is_redis = True
+            break
+    return is_redis
+
+
+def _ini_redis():
+    cmd = "which redis-server"
+    code, stdout, stderr = shell_cmd(cmd)
+    if not os.path.isfile(stdout):
+        subprocess.run("yum install -y redis", shell=True)
+
+
+def _start_redis():
+    cmd = ("echo 1024 > /proc/sys/net/core/somaxconn; "
+           "sysctl vm.overcommit_memory=1;echo never > /sys/kernel/mm/transparent_hugepage/enabled"
+           )
+    subprocess.run(cmd, shell=True)
+    subprocess.run("redis-server /etc/redis.conf", shell=True)
+
+
+def redis_deamond():
+    '''redis后台守护'''
+    if not _has_redis():
+        _ini_redis()
+        _start_redis()
+    print('redis 运行成功')
+
+if __name__ == '__main__':
+    redis_deamond()

+ 55 - 0
monitor/supervisord/build_monitor/system_monitor.py

@@ -0,0 +1,55 @@
+# coding:utf-8
+
+
+'''系统监控,目前仅支持disk,memory监控'''
+
+import os
+import sys
+import json
+import time
+
+
+local_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if local_path not in sys.path:
+    sys.path.append(local_path)
+
+
+from monitor.utils.comm_tools import shell_cmd, get_redis_connection
+
+
+class SystemdMonitor:
+    def __init__(self):
+        self.con = get_redis_connection()
+        self.table = 'yf_sys_monitor'
+
+    def get_memory_info(self):
+        cmd = "free -h"
+        code, stdout, stderr = shell_cmd(cmd)
+        if code == 0:
+            content = ' ' * 16 + stdout
+            self.con.hset(self.table, 'memory', content)
+
+    def get_disk_info(self):
+        cmd = "df -h"
+        code, stdout, stderr = shell_cmd(cmd)
+        if code == 0:
+            line_list = []
+            for line in stdout.splitlines():
+                temp_key = line.split()[0].strip()
+                if temp_key and 'tmpfs' in temp_key:
+                    continue
+                line_list.append(line)
+            content = '\n'.join(line_list)
+            self.con.hset(self.table, 'disk', content)
+
+    def run(self):
+        while True:
+            self.get_disk_info()
+            self.get_memory_info()
+            time.sleep(1)
+
+
+if __name__ == "__main__":
+    SystemdMonitor().run()
+
+

+ 4 - 0
monitor/supervisord/config/demo_env/dingding.yml

@@ -0,0 +1,4 @@
+# 钉钉机器人配置文件
+
+webhook: 'https://oapi.dingtalk.com/robot/send?access_token=11532b3909549951f493087e873ae603fda52011940da33c2a33992a4674bb20'
+secret: 'SECf2d2b723c60120b1dcd66da866423e33977d1bda555124b00505067e8282b1e2'

+ 15 - 0
monitor/supervisord/config/demo_env/monitor.yml

@@ -0,0 +1,15 @@
+# 监控配置文件
+
+# 所监控的目录名称,目录下所有符合 filter 和 exclude的文件都自动加入supervisord管理器中
+'/root/project/yunfei_monitor/monitor/supervisord/monitor_test':
+  # 需要后台执行的python脚本的解释器绝对路径
+  name: '本地测试'
+  # 运行程序的 Linux 用户
+  user: 'root'
+  executable: '/root/.pyenv/versions/yunfei_supervisord/bin/python'
+  filter:  # 只匹配.py *.monitor.py后缀的文件
+    - '*.py'
+    - '*_monitor.py'
+  exclude:    # 排除以__, ex_开头的文件
+    - '__*'
+    - 'ex_*'

+ 7 - 0
monitor/supervisord/config/demo_env/redis.yml

@@ -0,0 +1,7 @@
+# redis配置信息
+
+connection:
+  host: '127.0.0.1'
+  port: 6379
+  db: 6
+  password: null

+ 6 - 0
monitor/supervisord/monitor_test/ceshi.py

@@ -0,0 +1,6 @@
+# coding:utf-8
+
+import time
+while True:
+    print('1')
+    time.sleep(3)

+ 4 - 0
monitor/supervisord/monitor_test/test2.py

@@ -0,0 +1,4 @@
+import time
+
+print('kkkkkkkkkkkk')
+

+ 246 - 0
monitor/supervisord/yf_supervisord.py

@@ -0,0 +1,246 @@
+# coding:utf-8
+
+import os
+import sys
+import fs
+import psutil
+import subprocess
+from crontab import CronTab
+
+local_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if local_path not in sys.path:
+    sys.path.append(local_path)
+
+from utils.comm_tools import shell_cmd, get_monitor_config
+
+
+class SupervisorTools:
+    def __init__(self):
+        supervisor_dir = "/data/supervisor"
+        self.base_config_file = "/etc/supervisor/supervisord.conf"
+
+        if supervisor_dir[-1] == '/':
+            supervisor_dir = supervisor_dir[: -1]
+        temp_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+        utils_dir = os.path.join(temp_dir, 'utils')
+        self.exception_warning_file = os.path.join(utils_dir, 'exception_warning.py')
+        self.config_base = supervisor_dir
+
+        self.python_bin = sys.executable
+        self.bin_dir = os.path.dirname(self.python_bin)
+        self.supervisord = os.path.join(self.bin_dir, 'supervisord')
+        self.supervisorctl = os.path.join(self.bin_dir, 'supervisorctl')
+        self.config_dir = os.path.join(self.config_base, 'config.d')
+        self.log_dir = os.path.join(self.config_base, 'log')
+
+        tmp_dir = os.path.dirname(self.base_config_file)
+        if not os.path.isdir(tmp_dir):
+            os.makedirs(tmp_dir)
+        if not os.path.isdir(self.config_dir):
+            os.makedirs(self.config_dir)
+        if not os.path.isdir(self.log_dir):
+            os.makedirs(self.log_dir)
+
+    def _get_supervisor_app(self):
+        '''获取supervisor所管理的程序'''
+        status, app_name_list = False, []
+        cmd = '{} status'.format(self.supervisorctl)
+        code, stdout, stderr = shell_cmd(cmd)
+
+        if code >= 0:
+            for line in stdout.splitlines():
+                tmp_line = line.split()
+                if len(tmp_line) > 1:
+                    app_name = tmp_line[0].strip()
+                    app_name_list.append(app_name)
+            status, msg = True, set(app_name_list)
+        else:
+            msg = '获取supervisor所管理的程序失败: {}'.format(stderr)
+        return status, msg
+
+    def _make_task_conf(self, app_name, dir_name, cmd, user):
+        '''生成任务配置文件'''
+        log_dir = os.path.join(self.log_dir, app_name)
+        if not os.path.isdir(log_dir):
+            os.makedirs(log_dir)
+        conf_path = os.path.join(self.config_dir, '{}.conf'.format(app_name))
+        log_file = os.path.join(log_dir, '{}.log'.format(app_name))
+
+        conf_str = '''
+#程序唯一名称
+[program:{app_name}]
+
+#程序路径                                       
+directory={dir_name}
+
+#运行程序的命令                                       
+command={cmd}
+
+#标准日志输出位置
+stdout_logfile={log_file}
+
+autostart=true                          #supervisord启动后,同时启动此程序                                  
+startsecs=5                             #启动5秒后没有异常退出,就表示进程正常启动了,默认为1秒
+autorestart=true                        #程序退出后自动重启
+startretries=3                          #启动失败自动重试次数,默认是3                   
+user={user}                               #用哪个用户启动进程,默认是root                                      
+redirect_stderr=true                    #把stderr重定向到stdout标准输出,默认false            
+stdout_logfile_maxbytes=50MB            #stdout标准输出日志文件大小,日志文件备份数
+stdout_logfile_backups=10
+stopasgroup=true                        # 停止或终止进程时,同时结束进程包含的子进程 
+killasgroup=true
+
+'''.format(app_name=app_name, dir_name=dir_name, cmd=cmd, log_file=log_file, user=user)
+        with open(conf_path, 'w') as fw:
+            fw.write(conf_str)
+
+    def _make_supervisor_conf(self):
+        cmd = '{} -u {}'.format(self.python_bin, self.exception_warning_file)
+        conf = '''
+[unix_http_server]
+file={supervisor_dir}/supervisor.sock   ; the path to the socket file
+;chmod=0700                 ; socket file mode (default 0700)
+;chown=nobody:nogroup       ; socket file uid:gid owner
+;username=user              ; default is no username (open server)
+;password=123               ; default is no password (open server)
+
+[inet_http_server]         ; inet (TCP) server disabled by default
+port=127.0.0.1:59001        ; ip_address:port specifier, *:port for all iface
+username=yunfei_yanfabu              ; default is no username (open server)
+password=yanfabu6021               ; default is no password (open server)
+
+[supervisord]
+logfile={supervisor_dir}/supervisord.log ; main log file; default $CWD/supervisord.log
+logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
+logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
+loglevel=info                ; log level; default info; others: debug,warn,trace
+pidfile={supervisor_dir}/supervisord.pid ; supervisord pidfile; default supervisord.pid
+nodaemon=false               ; start in foreground if true; default false
+silent=false                 ; no logs to stdout if true; default false
+minfds=1024                  ; min. avail startup file descriptors; default 1024
+minprocs=200                 ; min. avail process descriptors;default 200
+;umask=022                   ; process file creation umask; default 022
+user=root                    ; setuid to this UNIX account at startup; recommended if root
+;identifier=supervisor       ; supervisord identifier, default is 'supervisor'
+;directory=/tmp              ; default is not to cd during start
+;nocleanup=true              ; don't clean up tempfiles at start; default false
+;childlogdir=/tmp            ; 'AUTO' child log dir, default $TEMP
+;environment=KEY="value"     ; key value pairs to add to environment
+;strip_ansi=false            ; strip ansi escape codes in logs; def. false
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
+
+[supervisorctl]
+serverurl=unix://{supervisor_dir}/supervisor.sock
+;username=chris              ; should be same as in [*_http_server] if set
+;password=123                ; should be same as in [*_http_server] if set
+;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
+;history_file=~/.sc_history  ; use readline history if available
+
+[eventlistener:crash_warning]
+command={exception_warning_file}
+events=PROCESS_STATE
+redirect_stderr=false
+
+[include]
+files = {supervisor_dir}/config.d/*.conf
+
+'''.format(exception_warning_file=cmd, supervisor_dir=self.config_base)
+        print(cmd)
+        with open(self.base_config_file, 'w') as fw:
+            fw.write(conf)
+
+    def _update_task(self):
+        '''启动添加的程序'''
+        status, msg = True, 'supervisor管理器启动程序成功'
+        cmd = '{} update all'.format(self.supervisorctl)
+        code, stdout, stderr = shell_cmd(cmd)
+        if code != 0:
+            status, msg = False, 'supervisor管理器启动程序失败:{}\n{}'.format(stdout, stderr)
+        return status, msg
+
+    def make_task(self):
+        '''创建supervisor任务并启动'''
+        status, msg = self._get_supervisor_app()
+        if not status:
+            return status, msg
+        app_name_set, app_name_list = msg, []
+        monitor_config = get_monitor_config()
+        with fs.open_fs('/') as root_fs:
+            for script_dir, item in monitor_config.items():
+                if not os.path.isdir(script_dir):
+                    continue
+                file_filter, file_exclude = item['filter'], item.get('exclude', [])
+                python_bin, env_name, cmd_user = item['executable'], item['name'], item['user']
+                if not file_exclude:
+                    file_exclude = None
+                if not file_filter:
+                    file_filter = None
+                for path in root_fs.walk.files(script_dir, filter=file_filter, exclude=file_exclude, max_depth=1):
+                    dir_name, file_name = os.path.split(path)
+                    index = file_name.rfind('.')
+                    app_name = file_name[:index] if index != -1 else file_name
+                    app_name = "{}_{}".format(env_name, app_name)
+                    if app_name in app_name_set:
+                        continue
+                    cmd = '{} -u {}'.format(python_bin, path)
+                    self._make_task_conf(app_name=app_name, dir_name=dir_name, cmd=cmd, user=cmd_user)
+                    app_name_list.append(app_name)
+
+        status, msg = True, 'supervisor管理器启动程序成功'
+        if app_name_list:
+            status, msg = self._update_task()
+        return status, msg
+
+    def _check_supervisord(self):
+        '''	检测supervisor管理器进程是否运行'''
+        is_run = False
+        for proc in psutil.process_iter():
+            proc_name = proc.name().lower()
+            if proc_name == 'supervisord':
+                is_run = True
+                break
+        return is_run
+
+    def start_supervisord(self):
+        '''启动supervisor管理器'''
+        msg = ''
+        if not os.path.isfile(self.supervisord):
+            msg = 'supervisor未安装,请先安装supervisor'
+        else:
+            is_run = self._check_supervisord()
+            if not is_run:
+                self._make_supervisor_conf()
+                cmd = "{} -c {}".format(self.supervisord, self.base_config_file)
+                result = subprocess.run(cmd, shell=True)
+                if result.returncode == 0:
+                    is_run = True
+                else:
+                    msg = '启动supervisor管理器失败, 请检查'
+
+            if is_run:
+                status, msg = self.make_task()
+        return msg
+
+
+def _init_crond():
+    cron = CronTab(user='root')
+    python_bin = sys.executable
+    main_file = os.path.abspath(__file__)
+    cmd = "{} {}".format(python_bin, main_file)
+    for job in cron:
+        if job.command in cmd:
+            break
+    else:
+        new_job = cron.new(cmd, comment="supervise监控脚本,禁止修改")
+        new_job.minute.every(2)
+        cron.write()
+
+
+if __name__ == '__main__':
+    _init_crond()
+
+    st = SupervisorTools()
+    msg = st.start_supervisord()
+    print(msg)

+ 0 - 0
monitor/utils/__init__.py


+ 85 - 0
monitor/utils/comm_tools.py

@@ -0,0 +1,85 @@
+# coding:utf-8
+
+import os
+import sys
+import yaml
+import redis
+import subprocess
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+def _get_config_dir():
+    config_dir = os.path.join(BASE_DIR, 'supervisord/config/production_env')
+    return config_dir
+
+
+def _load_ymal(path):
+    with open(path, 'r') as fr:
+        data = yaml.load(fr, yaml.FullLoader)
+    if data is None:
+        data = {}
+    return data
+
+
+def get_monitor_config():
+    '''获取监控配置'''
+    _config_dir = _get_config_dir()
+    monitor_path = os.path.join(_config_dir, 'monitor.yml')
+    monitor_data = _load_ymal(monitor_path)
+    build_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'build_monitor')
+    monitor_data[build_dir] = {
+        'name': 'build',
+        'executable': sys.executable,
+        'filter': ['*_monitor.py'],
+        'exclude': []
+    }
+    return monitor_data
+
+
+def get_redis_connection():
+    '''获取redis连接'''
+    _config_dir = _get_config_dir()
+    redis_path = os.path.join(_config_dir, 'redis.yml')
+    result = _load_ymal(redis_path)
+    data = result['connection']
+    host, port, db = data['host'], int(data['port']), int(data['db'])
+    try:
+        password = data['password']
+    except KeyError as e:
+        password = None
+    connection = redis.Redis(host=host, port=port, db=db, password=password, decode_responses=True)
+    return connection
+
+
+def get_dingding_config():
+    _config_dir = _get_config_dir()
+    path = os.path.join(_config_dir, 'dingding.yml')
+    data = _load_ymal(path)
+    return data
+
+
+def shell_cmd(cmd, is_wait=True):
+    '''执行shell命令
+    :param is_wait 是否阻塞等待命令运行结束,Ture 表示阻塞模式
+    :return
+        code, stdout, stderr
+
+        code            命令运行状态码,0表示运行成功
+        stdout          命令运行输出结果
+        stderr          命令运行错误信息
+    '''
+    code, stdout, stderr = 0, '', ''
+    proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    if is_wait:
+        stdout, stderr = proc.communicate()
+        code = proc.returncode
+        stdout = stdout.decode('utf-8').strip()
+        stderr = stderr.decode('utf-8').strip()
+    return code, stdout, stderr
+
+
+if __name__ == "__main__":
+    con = get_redis_connection()
+    print(con)
+

+ 115 - 0
monitor/utils/exception_warning.py

@@ -0,0 +1,115 @@
+# coding:utf-8
+
+'''
+异常警告事件监控脚本:supervisor程序异常中止时候自动发送异常警告消息到redis中
+auth : zhaiyifei
+time : 2021-12-14
+'''
+
+
+import os
+import sys
+import json
+import datetime
+from supervisor import childutils
+
+local_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if local_path not in sys.path:
+    sys.path.append(local_path)
+
+from monitor.utils.comm_tools import get_redis_connection
+
+
+def usage(exitstatus=255):
+    sys.exit(exitstatus)
+
+
+class CrashExceptionWarning:
+    def __init__(self, programs, anyy):
+        self.programs = programs
+        self.any = anyy
+        self.stdin = sys.stdin
+        self.stdout = sys.stdout
+        self.stderr = sys.stderr
+        self.event_dict = {}
+        self.con = get_redis_connection()
+
+    def _get_now(self):
+        '''获取时间'''
+        now = datetime.datetime.now()
+        now_str = now.strftime('%Y-%m-%d %H:%M:%S')
+        return now, now_str
+
+    def _clean_event(self):
+        '''清理过期事件,超过30秒既为过期事件'''
+        now = self._get_now()[0]
+        key_list = []
+        for key, value in self.event_dict.items():
+            diff = now - value[0]
+            if diff.seconds > 30:
+                key_list.append(key_list)
+
+        for key in key_list:
+            if key in self.event_dict:
+                self.event_dict.pop(key)
+
+    def send_redis(self, content):
+        '''推送消息redis'''
+        data = json.dumps(content)
+        self.con.rpush('yf_dingding', data)
+
+    def runforever(self, test=False):
+        while True:
+            headers, payload = childutils.listener.wait(
+                self.stdin, self.stdout)
+            pheaders, pdata = childutils.eventdata(payload + '\n')
+
+            app_name = pheaders['processname']
+            if app_name == 'crasheding':
+                childutils.listener.ok(self.stdout)
+                continue
+
+            eventname = headers['eventname']
+            if eventname != 'PROCESS_STATE_EXITED':
+                now_str = self._get_now()[1]
+                if eventname == 'PROCESS_STATE_RUNNING':
+                    if app_name in self.event_dict:
+                        msg = '[{}] 程序于 {} 发生异常中止,并在 {} 自动重启 成功'.format(
+                            app_name, self.event_dict[app_name], now_str)
+                        self.event_dict.pop(app_name)
+                        self.send_redis({'content': msg})
+                elif eventname == 'PROCESS_STATE_FATAL':
+                    msg = '[{}] 程序于 {} 启动失败'.format(app_name, now_str)
+                    if app_name in self.event_dict:
+                        msg = '[{}] 程序于 {} 发生异常中止,并在 {} 自动重启 失败'.format(
+                            app_name, self.event_dict[app_name], now_str)
+                        self.event_dict.pop(app_name)
+                    self.send_redis({'content': msg, 'monitor_list': ['disk', 'memory']})
+
+                childutils.listener.ok(self.stdout)
+                continue
+
+            if int(pheaders['expected']):
+                childutils.listener.ok(self.stdout)
+                continue
+
+            self.stderr.write('unexpected exit, mailing\n')
+            self.stderr.flush()
+
+            self.event_dict[app_name] = self._get_now()[1]
+            msg = '[{}] 程序于 {} 发生异常中止'.format(
+                app_name, self.event_dict[app_name], self._get_now()[1])
+            self.send_redis({'content': msg, 'monitor_list': ['disk', 'memory']})
+
+            childutils.listener.ok(self.stdout)
+
+
+def main():
+    programs = []
+    prog = CrashExceptionWarning(programs, True)
+    prog.runforever()
+
+
+if __name__ == '__main__':
+    main()
+

+ 61 - 0
monitor/utils/supervisord_utils.py

@@ -0,0 +1,61 @@
+# coding:utf-8
+import os.path
+from xmlrpc.client import ServerProxy
+
+
+class SupervisordUtils:
+    def __init__(self):
+        url = "http://yunfei_yanfabu:yanfabu6021@127.0.0.1:59001/RPC2"
+        self.server = ServerProxy(url)
+        self.supervisor = self.server.supervisor
+
+    def is_exists(self, name):
+        exists = True
+        try:
+            self.supervisor.getProcessInfo(name)
+        except Exception as e:
+            exists = False
+        return exists
+
+    def list(self, name):
+        result = []
+        for info in self.supervisor.getAllProcessInfo():
+            if name and (name not in info['name']):
+                continue
+            result.append(info)
+        return result
+
+    def restart(self, name):
+        try:
+            info = self.supervisor.getProcessInfo(name)
+            statename = info['statename']
+            if statename == 'RUNNING':
+                self.supervisor.stopProcess(name)
+        except Exception as e:
+            pass
+
+        self.supervisor.removeProcessGroup(name)
+        self.supervisor.reloadConfig()
+        self.supervisor.addProcessGroup(name)
+
+    def remove(self, name):
+        try:
+            info = self.supervisor.getProcessInfo(name)
+            statename = info['statename']
+            if statename == 'RUNNING':
+                self.supervisor.stopProcess(name)
+        except Exception as e:
+            pass
+
+        self.supervisor.removeProcessGroup(name)
+        path = '/data/supervisor/config.d'
+        file_path = os.path.join(path, name)
+        if os.path.isfile(file_path):
+            os.remove(file_path)
+
+
+if __name__ == '__main__':
+    su = SupervisordUtils()
+    print(su.list())
+    su.restart('本地测试_test2')
+    # su.restart('本地测试_test2')

+ 18 - 0
monitor/yfmonitor_gunicorn.py

@@ -0,0 +1,18 @@
+#gunicorn.py
+
+import os
+
+base_dir = os.path.dirname(os.path.abspath(__file__))
+log_dir = os.path.join(base_dir, 'log')
+if not os.path.isdir(log_dir):
+    os.makedirs(log_dir)
+bind = '0.0.0.0:56789'  # 绑定ip和端口号
+backlog = 1024  # 监听队列
+# chdir = '/data/szmg/szmg/bigdata/'  # gunicorn要切换到的目的工作目录
+chdir = base_dir
+worker_class = 'gevent'  # 使用gevent模式,还可以使用sync 模式,默认的是sync模式
+#workers = multiprocessing.cpu_count() * 2 + 1  # 进程数(多进程数据不互通,默认workers为1,单进程模式)
+loglevel = 'debug'  # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置
+errorlog = os.path.join(log_dir, "error.log") #错误日志
+accesslog = os.path.join(log_dir, "access.log") #访问日志
+daemon = True

+ 32 - 0
requirements.txt

@@ -0,0 +1,32 @@
+appdirs==1.4.4
+attrs==21.4.0
+certifi==2021.10.8
+charset-normalizer==2.0.9
+Deprecated==1.2.13
+DingtalkChatbot==1.5.3
+Django==2.2.27
+django-filter==21.1
+djangorestframework==3.13.1
+drf-spectacular==0.22.0
+fs==2.4.13
+idna==3.3
+importlib-metadata==4.8.3
+inflection==0.5.1
+jsonschema==3.2.0
+Markdown==3.3.6
+psutil==5.8.0
+pyrsistent==0.18.0
+python-crontab==2.5.1
+python-dateutil==2.8.1
+pytz==2021.3
+PyYAML==6.0
+redis==4.0.2
+requests==2.26.0
+six==1.16.0
+sqlparse==0.4.2
+supervisor==4.2.2
+typing_extensions==4.1.1
+uritemplate==4.1.1
+urllib3==1.26.7
+wrapt==1.13.3
+zipp==3.6.0

+ 45 - 0
restart.sh

@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+GUNICORN_NAME="yfmonitor_gunicorn.py"
+
+function start_service() {
+    echo "启动中请稍等"
+
+    local DIR_NAME=$(cd `dirname "$0"`; pwd)
+    local BASE_DIR="${DIR_NAME}/monitor"
+    local start_file="${BASE_DIR}/${GUNICORN_NAME}"
+
+    source activate yunfei_monitor  >/dev/null 2>&1
+    python ${BASE_DIR}/supervisord/yf_supervisord.py
+    gunicorn -c "${start_file}" monitor.wsgi:application
+    sleep 1
+
+    local ret=$(ps axu | grep "${GUNICORN_NAME}" | grep -v grep | wc -l)
+    if [[ ${ret} -gt 0 ]];then
+        echo "服务启动成功"
+    else
+        echo -e "\033[31m 启动失败请检查 \033[0m"
+    fi
+}
+
+function stop_service() {
+    echo "停止服务中请稍等"
+    ps axu | grep "${GUNICORN_NAME}" | grep -v grep | awk '{print $2}' | xargs kill -9 >/dev/null 2>&1
+    local ret=$(ps axu | grep "${GUNICORN_NAME}"| grep -v grep | wc -l)
+    if [[ ${ret} -eq 0 ]];then
+        echo "服务停止成功"
+        start_service
+    else
+        echo -e "\033[31m 停止失败请检查 \033[0m"
+    fi
+}
+
+#chown -R www:www /var/lib/nginx
+#chown -R www:www /data/szmg/www
+#chown -R www:www /data/szmg/img
+#chown -R www:www /data/szmg/app
+
+stop_service
+
+
+

+ 19 - 0
stop.sh

@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+GUNICORN_NAME="yfmonitor_gunicorn.py"
+
+function stop_service() {
+    echo "停止服务中请稍等"
+    ps axu | grep "${GUNICORN_NAME}" | grep -v grep | awk '{print $2}' | xargs kill -9 >/dev/null 2>&1
+    local ret=$(ps axu | grep "${GUNICORN_NAME}" | grep -v grep | wc -l)
+    if [[ ${ret} -eq 0 ]];then
+        echo "服务停止成功"
+    else
+        echo -e "\033[31m 停止失败请检查 \033[0m"
+    fi
+}
+
+stop_service
+
+
+