├── apps ├── __init__.py ├── cmdb │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── views │ │ ├── __init__.py │ │ ├── domain.py │ │ ├── deploy_chart.py │ │ └── server.py │ ├── apps.py │ ├── serializers │ │ ├── __init__.py │ │ └── server.py │ ├── admin.py │ ├── urls.py │ └── tests.py ├── k8s │ ├── __init__.py │ ├── views │ │ ├── __init__.py │ │ ├── cluster.py │ │ ├── events.py │ │ ├── logs.py │ │ ├── namespace.py │ │ ├── scale.py │ │ └── pod.py │ ├── migrations │ │ └── __init__.py │ ├── serializers │ │ ├── __init__.py │ │ ├── pod.py │ │ ├── deployment.py │ │ └── node.py │ ├── models.py │ ├── admin.py │ ├── tests.py │ ├── apps.py │ └── urls.py ├── rbac │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── serializers │ │ ├── __init__.py │ │ ├── menu_serializers.py │ │ ├── permission_serializers.py │ │ └── roles_serializers.py │ ├── tests.py │ ├── apps.py │ ├── views │ │ ├── __init__.py │ │ ├── menu.py │ │ ├── permission.py │ │ └── role.py │ ├── admin.py │ ├── urls.py │ └── models.py ├── account │ ├── __init__.py │ ├── views │ │ └── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto_20210531_1520.py │ │ ├── 0003_auto_20210601_1853.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── serializers │ │ ├── __init__.py │ │ └── users.py │ ├── urls.py │ ├── models.py │ └── admin.py └── application │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ ├── 0002_auto_20210531_1520.py │ ├── 0003_auto_20210601_1853.py │ └── 0001_initial.py │ ├── views │ ├── __init__.py │ └── diagnosis.py │ ├── apps.py │ ├── tests.py │ ├── serializers │ ├── __init__.py │ └── app_diagnosis.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── utils │ └── check_heartbeat.py ├── controller ├── temphosts ├── test.yaml ├── ansible │ ├── utils.py │ ├── test_win.py │ ├── test_runner.py │ ├── test_yaml.py │ ├── mongo_logs.py │ ├── test_inventory.py │ ├── display.py │ ├── test_copy.py │ ├── test_cmd.py │ └── inventory.py └── kube_pod_exec.py ├── img ├── app.png ├── job.png ├── job2.png ├── node.png ├── role.png ├── login.png ├── method.png ├── scale.png ├── server.png ├── server2.png ├── container.png ├── send_file.png ├── workload.png ├── yaml_edit.png ├── container2.png ├── k8s_detail2.png ├── node_detail1.png ├── server_ssh.png ├── container_logs.png ├── container_ssh.png ├── deployment_detail1.png └── deployment_detail2.png ├── metrices ├── metrics_proxy └── metrics_proxy.yaml ├── devops_api ├── __init__.py ├── wsgi.py ├── urls.py ├── asgi.py └── celery.py ├── utils ├── kubernetes_units.txt ├── throttle.py ├── csrf_disable.py ├── backends.py ├── pagination.py ├── http_response.py ├── http_requests.py ├── tree.py ├── prpcrypt.py ├── jwt_token.py ├── authorization.py ├── code.py ├── ws_auth.py ├── permissions.py └── time_utils.py ├── tasks ├── __init__.py ├── domain.py ├── ansible_cmd.py ├── aliyun.py └── application.py ├── exception └── my_exception.py ├── doc ├── v2_init_sql │ ├── rbac_role.sql │ ├── rbac_menu.sql │ └── rbac_permission.sql ├── x-request-id.md └── 1_init.md ├── manage.py ├── .github └── FUNDING.yml ├── .env ├── requirements.txt ├── README.md └── consumer ├── application.py ├── webssh.py └── ecs_webssh.py /apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cmdb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/k8s/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rbac/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controller/temphosts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/application/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/k8s/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/account/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cmdb/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/k8s/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/k8s/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rbac/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/rbac/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/application/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/app.png -------------------------------------------------------------------------------- /img/job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/job.png -------------------------------------------------------------------------------- /img/job2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/job2.png -------------------------------------------------------------------------------- /img/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/node.png -------------------------------------------------------------------------------- /img/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/role.png -------------------------------------------------------------------------------- /img/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/login.png -------------------------------------------------------------------------------- /img/method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/method.png -------------------------------------------------------------------------------- /img/scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/scale.png -------------------------------------------------------------------------------- /img/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/server.png -------------------------------------------------------------------------------- /img/server2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/server2.png -------------------------------------------------------------------------------- /apps/k8s/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /img/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/container.png -------------------------------------------------------------------------------- /img/send_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/send_file.png -------------------------------------------------------------------------------- /img/workload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/workload.png -------------------------------------------------------------------------------- /img/yaml_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/yaml_edit.png -------------------------------------------------------------------------------- /apps/account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/k8s/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /apps/k8s/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/rbac/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /img/container2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/container2.png -------------------------------------------------------------------------------- /img/k8s_detail2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/k8s_detail2.png -------------------------------------------------------------------------------- /img/node_detail1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/node_detail1.png -------------------------------------------------------------------------------- /img/server_ssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/server_ssh.png -------------------------------------------------------------------------------- /img/container_logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/container_logs.png -------------------------------------------------------------------------------- /img/container_ssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/container_ssh.png -------------------------------------------------------------------------------- /metrices/metrics_proxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/metrices/metrics_proxy -------------------------------------------------------------------------------- /apps/cmdb/views/__init__.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /img/deployment_detail1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/deployment_detail1.png -------------------------------------------------------------------------------- /img/deployment_detail2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnsjia/devops-api/HEAD/img/deployment_detail2.png -------------------------------------------------------------------------------- /apps/application/views/__init__.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /apps/cmdb/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CmdbConfig(AppConfig): 5 | name = 'cmdb' 6 | -------------------------------------------------------------------------------- /apps/k8s/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class K8SConfig(AppConfig): 5 | name = 'apps.k8s' 6 | -------------------------------------------------------------------------------- /apps/rbac/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RbacConfig(AppConfig): 5 | name = 'rbac' 6 | -------------------------------------------------------------------------------- /devops_api/__init__.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | pymysql.version_info = (1,4,14, 'final', 0) 4 | pymysql.install_as_MySQLdb() 5 | -------------------------------------------------------------------------------- /apps/account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = 'account' 6 | -------------------------------------------------------------------------------- /apps/application/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApplicationConfig(AppConfig): 5 | name = 'application' 6 | -------------------------------------------------------------------------------- /metrices/metrics_proxy.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http: 3 | address: "0.0.0.0" 4 | port: "9988" 5 | api_port: "8081" 6 | token: "123456" 7 | accept_hosts: "^*" -------------------------------------------------------------------------------- /apps/rbac/views/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @File : __init__.py.py 4 | # @Author: 风哥 5 | # @Email: gujiwork@outlook.com 6 | # @Date : 2020/12/1 7 | # @Desc : 8 | -------------------------------------------------------------------------------- /controller/test.yaml: -------------------------------------------------------------------------------- 1 | - hosts: 192.168.1.11 2 | #gather_facts: F #开启debug 3 | tasks: 4 | - name: nslookup 5 | shell: ping -c 4 www.baidu.com 6 | ignore_errors: True 7 | register: tomcat_out #定义变量存储返回的结果 -------------------------------------------------------------------------------- /apps/application/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | 5 | import requests 6 | 7 | 8 | # check = requests.get(url='http://192.168.1.211:8563/api', timeout=2) 9 | # print(check.content) -------------------------------------------------------------------------------- /utils/kubernetes_units.txt: -------------------------------------------------------------------------------- 1 | # memory units 2 | 3 | kmemunits = 1 = [kmemunits] 4 | Ki = 1024 * kmemunits 5 | Mi = Ki^2 6 | Gi = Ki^3 7 | Ti = Ki^4 8 | Pi = Ki^5 9 | Ei = Ki^6 10 | 11 | # cpu units 12 | 13 | kcpuunits = 1 = [kcpuunits] 14 | m = 1/1000 * kcpuunits 15 | k = 1000 * kcpuunits 16 | M = k^2 17 | G = k^3 18 | T = k^4 19 | P = k^5 20 | E = k^6 -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/14 0014 下午 12:15 11 | @Author: micheng. 12 | @File: __init__.py.py 13 | """ 14 | -------------------------------------------------------------------------------- /apps/cmdb/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/21 0021 下午 12:27 11 | @Author: micheng. 12 | @File: __init__.py.py 13 | """ 14 | -------------------------------------------------------------------------------- /apps/account/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/21 0021 下午 12:27 11 | @Author: micheng. 12 | @File: __init__.py.py 13 | """ 14 | -------------------------------------------------------------------------------- /apps/application/serializers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/31 0031 下午 4:39 11 | @Author: micheng. 12 | @File: __init__.py.py 13 | """ 14 | -------------------------------------------------------------------------------- /apps/rbac/admin.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | from django.contrib import admin 9 | from apps.rbac.models import Role, Permission, Menu 10 | 11 | admin.site.register(Role) 12 | admin.site.register(Permission) 13 | admin.site.register(Menu) -------------------------------------------------------------------------------- /devops_api/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for pigs-devops-apps project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devops_api.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /utils/throttle.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time: 2020/9/29 14:25 6 | @Author: yu.jiang 7 | @License: Apache License 8 | @File: throttle.py 9 | 10 | """ 11 | 12 | from rest_framework.throttling import SimpleRateThrottle 13 | 14 | 15 | class VisitThrottle(SimpleRateThrottle): 16 | # key名,需要在settings.py中配置 17 | scope = "user" 18 | 19 | def get_cache_key(self, request, view): 20 | return self.get_ident(request) -------------------------------------------------------------------------------- /apps/application/migrations/0002_auto_20210531_1520.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-31 15:20 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('application', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='diagnosis', 15 | options={'ordering': ['-id'], 'verbose_name': '应用诊断', 'verbose_name_plural': '应用诊断'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /tasks/domain.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/14 0014 下午 1:38 11 | @Author: micheng. 12 | @File: domain.py 13 | """ 14 | 15 | 16 | from celery import shared_task 17 | 18 | 19 | @shared_task 20 | def sync_domain(): 21 | print("10s 同步一次域名") 22 | return "10s 同步一次域名" -------------------------------------------------------------------------------- /apps/account/migrations/0002_auto_20210531_1520.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-31 15:20 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('account', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='history', 16 | name='created_at', 17 | field=models.CharField(default=datetime.datetime(2021, 5, 31, 15, 20, 33, 562114), max_length=20), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/account/migrations/0003_auto_20210601_1853.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-06-01 18:53 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('account', '0002_auto_20210531_1520'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='history', 16 | name='created_at', 17 | field=models.CharField(default=datetime.datetime(2021, 6, 1, 18, 53, 2, 139541), max_length=20), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/cmdb/views/domain.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/13 0013 下午 5:14 11 | @Author: micheng. 12 | @File: domain.py 13 | """ 14 | 15 | from rest_framework.views import APIView 16 | 17 | 18 | class DomainView(APIView): 19 | def get(self, request, *args, **kwargs): 20 | ... 21 | 22 | def post(self, request, *args, **kwargs): 23 | ... 24 | -------------------------------------------------------------------------------- /exception/my_exception.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/16 0016 下午 12:49 11 | @Author: micheng. 12 | @File: my_exception.py 13 | """ 14 | 15 | 16 | class AkException(BaseException): 17 | def __init__(self, *args, **kwargs): # real signature unknown 18 | pass 19 | 20 | def __str__(self): 21 | return "获取阿里云AK失败!" 22 | 23 | 24 | class AnsibleError(Exception): 25 | pass 26 | -------------------------------------------------------------------------------- /utils/csrf_disable.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/17 0017 下午 5:59 11 | @Author: micheng. 12 | @File: csrf_disable.py 13 | """ 14 | 15 | from rest_framework.authentication import SessionAuthentication 16 | 17 | 18 | class CsrfExemptSessionAuthentication(SessionAuthentication): 19 | 20 | def enforce_csrf(self, request): 21 | return # To not perform the csrf check previously happening 22 | -------------------------------------------------------------------------------- /apps/application/serializers/app_diagnosis.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/31 0031 下午 4:40 11 | @Author: micheng. 12 | @File: app_diagnosis.py 13 | """ 14 | 15 | from rest_framework import serializers 16 | 17 | from apps.application.models import Diagnosis 18 | 19 | 20 | class DiagnosisSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = Diagnosis 23 | fields = "__all__" 24 | -------------------------------------------------------------------------------- /apps/rbac/serializers/menu_serializers.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : menu_serializers.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | from apps.rbac.models import Menu 19 | 20 | 21 | class MenuSerializer(serializers.ModelSerializer): 22 | """ 23 | 角色列表序列化 24 | """ 25 | 26 | class Meta: 27 | model = Menu 28 | fields = '__all__' 29 | -------------------------------------------------------------------------------- /apps/application/admin.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | from django.contrib import admin 9 | 10 | from apps.application.models import Diagnosis 11 | 12 | 13 | # Register your models here. 14 | 15 | 16 | @admin.register(Diagnosis) 17 | class DiagnosisAdmin(admin.ModelAdmin): 18 | list_display = ['name', 'ip', 'user_name', 'is_active'] 19 | list_display_links = ['name', 'user_name'] 20 | list_filter = ['name', 'ip', 'user_name', 'is_active'] 21 | search_fields = ['name', 'ip', 'user_name'] 22 | -------------------------------------------------------------------------------- /apps/rbac/serializers/permission_serializers.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : permission_serializers.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | from apps.rbac.models import Permission 19 | 20 | 21 | class PermissionSerializer(serializers.ModelSerializer): 22 | """ 23 | 权限列化 24 | """ 25 | 26 | class Meta: 27 | model = Permission 28 | fields = '__all__' 29 | -------------------------------------------------------------------------------- /doc/v2_init_sql/rbac_role.sql: -------------------------------------------------------------------------------- 1 | SET FOREIGN_KEY_CHECKS=0; 2 | 3 | -- ---------------------------- 4 | -- Table structure for rbac_role 5 | -- ---------------------------- 6 | DROP TABLE IF EXISTS `rbac_role`; 7 | CREATE TABLE `rbac_role` ( 8 | `id` int(11) NOT NULL AUTO_INCREMENT, 9 | `name` varchar(32) NOT NULL, 10 | `desc` varchar(50) NOT NULL, 11 | PRIMARY KEY (`id`), 12 | UNIQUE KEY `name` (`name`) 13 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 14 | 15 | -- ---------------------------- 16 | -- Records of rbac_role 17 | -- ---------------------------- 18 | INSERT INTO `rbac_role` VALUES ('1', 'ops', '管理员'); 19 | INSERT INTO `rbac_role` VALUES ('2', 'develop', '开发人员'); 20 | INSERT INTO `rbac_role` VALUES ('3', 'test', '测试人员'); 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devops_api.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /utils/backends.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time: 2020/9/27 12:42 6 | @Author: yu.jiang 7 | @License: Apache License 8 | @File: backends.py 9 | 10 | """ 11 | 12 | from django.contrib.auth.backends import ModelBackend 13 | from django.db.models import Q 14 | 15 | from apps.account.models import User 16 | 17 | 18 | class MobileOrEmailBackend(ModelBackend): 19 | def authenticate(self, request, username=None, password=None, **kwargs): 20 | try: 21 | user = User.objects.get(Q(username=username) | Q(email=username) | Q(mobile=username)) 22 | if user.check_password(password): 23 | return user 24 | except Exception as e: 25 | return None 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | -------------------------------------------------------------------------------- /apps/application/migrations/0003_auto_20210601_1853.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-06-01 18:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('application', '0002_auto_20210531_1520'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='diagnosis', 15 | name='is_container', 16 | field=models.BooleanField(blank=True, default=0, null=True, verbose_name='是否容器'), 17 | ), 18 | migrations.AddField( 19 | model_name='diagnosis', 20 | name='process_name', 21 | field=models.CharField(blank=True, max_length=32, null=True, verbose_name='进程名'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/k8s/views/cluster.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from kubernetes import client, config 3 | from kubernetes.client import models 4 | import yaml 5 | # from devops.settings import BASE_DIR 6 | # k8s_api_config = os.path.join(BASE_DIR, 'apps', 'utils', 'config_test') 7 | 8 | config.load_kube_config(config_file=None) 9 | logger = logging.getLogger('default') 10 | app_v2 = client.CoreV1Api() 11 | app_v1 = client.AppsV1Api() 12 | logger.info("Getting k8s nodes...") 13 | 14 | res = app_v2.list_event_for_all_namespaces(field_selector="involvedObject.kind=Deployment," 15 | "involvedObject.name=v3-release") 16 | 17 | print(res) 18 | 19 | # re = yaml.safe_dump(res.to_dict(), sort_keys=False) 20 | # print(re) 21 | 22 | # print(yaml.safe_load(re)) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /apps/k8s/serializers/pod.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : pod.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/7 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | 19 | 20 | class PodSerializers(serializers.Serializer): 21 | """ 22 | Pod的序列化 23 | """ 24 | pod_ip = serializers.IPAddressField() 25 | namespace = serializers.CharField() 26 | name = serializers.CharField() 27 | host_ip = serializers.IPAddressField() 28 | status = serializers.JSONField() 29 | create_time = serializers.DateTimeField() 30 | restart_count = serializers.CharField() -------------------------------------------------------------------------------- /apps/account/serializers/users.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : users.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/28 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | from apps.account.models import User 19 | 20 | 21 | class UserInfoSerializer(serializers.ModelSerializer): 22 | """ 23 | 用户信息 24 | """ 25 | 26 | class Meta: 27 | model = User 28 | fields = ["id", "username", "email", "user_id", "mobile", "name", "sex", "position", 29 | "avatar", "is_active", "last_login", "roles", "is_staff" 30 | ] 31 | depth = 1 32 | -------------------------------------------------------------------------------- /apps/application/urls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/26 0026 上午 11:29 11 | @Author: micheng. 12 | @File: urls.py 13 | """ 14 | 15 | from django.conf.urls import url 16 | from django.urls import path 17 | 18 | from apps.application.views.diagnosis import ArtHasView, ArtHasInstallView, ListServiceNameView 19 | 20 | urlpatterns = [ 21 | url(r'^(?P[v1|v2]+)/diagnosis$', ArtHasView.as_view(), name='arthas'), 22 | url(r'^(?P[v1|v2]+)/diagnosis/agent$', ArtHasInstallView.as_view(), name='install_arthas'), 23 | url(r'^(?P[v1|v2]+)/diagnosis/service/list$', ListServiceNameView.as_view(), name='list_service_name'), 24 | 25 | ] 26 | -------------------------------------------------------------------------------- /apps/rbac/serializers/roles_serializers.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : roles_serializers.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | from apps.rbac.models import Role 19 | 20 | 21 | class RoleListSerializer(serializers.ModelSerializer): 22 | """ 23 | 角色列表序列化 24 | """ 25 | 26 | class Meta: 27 | model = Role 28 | fields = '__all__' 29 | depth = 1 30 | 31 | 32 | class RoleModifySerializer(serializers.ModelSerializer): 33 | """ 34 | 角色增删改序列化 35 | """ 36 | 37 | title = serializers.CharField(required=False) 38 | 39 | class Meta: 40 | model = Role 41 | fields = '__all__' 42 | 43 | -------------------------------------------------------------------------------- /utils/pagination.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : pagination.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework.pagination import PageNumberPagination 18 | 19 | 20 | class MyPageNumberPagination(PageNumberPagination): 21 | page_size = 10 22 | page_query_param = 'page' 23 | page_size_query_param = 'pageSize' 24 | max_page_size = 40 25 | 26 | 27 | class MenuPagination(PageNumberPagination): 28 | page_size = 100 29 | page_query_param = 'page' 30 | page_size_query_param = 'page_size' 31 | max_page_size = 100 32 | 33 | 34 | class MaxPagination(PageNumberPagination): 35 | page_size = 1000 36 | page_query_param = 'page' 37 | page_size_query_param = 'page_size' 38 | max_page_size = 1000 -------------------------------------------------------------------------------- /apps/k8s/serializers/deployment.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : deployment.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/7 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | 19 | 20 | class DeploymentSerializers(serializers.Serializer): 21 | """ 22 | Deployment的序列化 23 | """ 24 | namespace = serializers.CharField() 25 | name = serializers.CharField() 26 | labels = serializers.JSONField() 27 | create_time = serializers.DateTimeField() 28 | image = serializers.CharField() 29 | replicas = serializers.CharField() 30 | available_replicas = serializers.CharField() 31 | ready_replicas = serializers.CharField() 32 | unavailable_replicas = serializers.CharField() 33 | updated_replicas = serializers.CharField() 34 | -------------------------------------------------------------------------------- /doc/x-request-id.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://django-request-id.readthedocs.io/en/latest/ 5 | - pip install django-request-id 6 | ``` 7 | LOGGING= { 8 | "version": 1, 9 | "disable_existing_loggers": False, 10 | "filters": { 11 | "request_id": { 12 | "()": "request_id.logging.RequestIdFilter" 13 | } 14 | }, 15 | "formatters": { 16 | "console": { 17 | "format": "%(asctime)s - %(levelname)-5s [%(name)s] request_id=%(request_id)s %(message)s", 18 | "datefmt": "%H:%M:%S" 19 | } 20 | }, 21 | "handlers": { 22 | "console": { 23 | "level": "DEBUG", 24 | "filters": ["request_id"], 25 | "class": "logging.StreamHandler", 26 | "formatter": "console" 27 | } 28 | }, 29 | "loggers": { 30 | "": { 31 | "level": "DEBUG", 32 | "handlers": ["console"] 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | https://github.com/nigma/django-request-id 39 | 40 | https://github.com/dabapps/django-log-request-id 41 | 42 | https://www.jianshu.com/p/5e103e1eb017 -------------------------------------------------------------------------------- /utils/http_response.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : http_response.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/4/13 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework.response import Response 18 | 19 | 20 | class APIResponse(Response): 21 | """ 22 | 自定义Response返回数据: 23 | return APIResponse(data={"name":'11111111'},request_id='11111111') 24 | return APIResponse(data={"name":'11111111'}) 25 | return APIResponse(errcode='101', errmsg='错误',data={"name":'11111111'}, header={}) 26 | """ 27 | def __init__(self, errcode=0, errmsg=None, data=None, status=None, headers=None, **kwargs): 28 | dic = {'errcode': errcode, 'errmsg': errmsg} 29 | if data: 30 | dic = {'errcode': errcode, 'errmsg': errmsg, 'data': data} 31 | dic.update(kwargs) 32 | super().__init__(data=dic, status=status, headers=headers) 33 | 34 | -------------------------------------------------------------------------------- /devops_api/urls.py: -------------------------------------------------------------------------------- 1 | """pigs-devops-apps URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('api/account/', include('apps.account.urls')), 22 | path('api/k8s/', include('apps.k8s.urls')), 23 | path('api/rbac/', include('apps.rbac.urls')), 24 | path('api/cmdb/', include('apps.cmdb.urls')), 25 | path('api/application/', include('apps.application.urls')), 26 | ] -------------------------------------------------------------------------------- /apps/application/models.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | from django.db import models 10 | 11 | # Create your models here. 12 | 13 | 14 | class Diagnosis(models.Model): 15 | name = models.CharField(max_length=64, verbose_name='应用名称', null=True, blank=True) 16 | ip = models.GenericIPAddressField(verbose_name='应用IP', null=True, blank=True) 17 | user_name = models.CharField(max_length=32, verbose_name='安装用户', null=True, blank=True) 18 | is_active = models.BooleanField(verbose_name='是否在线', null=True, blank=True, default=0) 19 | process_name = models.CharField(verbose_name='进程名', null=True, blank=True, max_length=32) 20 | is_container = models.BooleanField(verbose_name='是否容器', null=True, blank=True, default=0) 21 | 22 | class Meta: 23 | db_table = 'diagnosis' 24 | verbose_name = '应用诊断' 25 | verbose_name_plural = verbose_name 26 | ordering = ['-id'] 27 | 28 | def __str__(self): 29 | return self.name 30 | -------------------------------------------------------------------------------- /utils/http_requests.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : http_requests.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/4/12 14 | # @Desc : 15 | """ 16 | 17 | import requests 18 | 19 | 20 | class RequestResult: 21 | 22 | @staticmethod 23 | def get(url, params=None): 24 | try: 25 | res = requests.get(url=url, params=params) 26 | return res.json() 27 | 28 | except BaseException as e: 29 | data = {'errcode': -1, 'errmsg': str(e)} 30 | return data 31 | 32 | @staticmethod 33 | def post(url, data=None, headers=None): 34 | if headers is None: 35 | headers = {} 36 | 37 | try: 38 | res = requests.post(url=url, data=data, headers=headers) 39 | return res.json() 40 | 41 | except BaseException as e: 42 | data = {'errcode': -1, 'errmsg': str(e)} 43 | return data 44 | -------------------------------------------------------------------------------- /utils/tree.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : tree.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | 18 | def tree_filter(serializer_data, filter_name=None): 19 | """ 20 | 树型化 21 | """ 22 | tree_dict = {} 23 | tree_data = [] 24 | try: 25 | for item in serializer_data: 26 | tree_dict[item['id']] = item 27 | for i in tree_dict: 28 | if filter_name: 29 | if i not in filter_name: 30 | continue 31 | if tree_dict[i]['pid']: 32 | pid = tree_dict[i]['pid'] 33 | parent = tree_dict[pid] 34 | parent.setdefault('children', []).append(tree_dict[i]) 35 | else: 36 | tree_data.append(tree_dict[i]) 37 | results = tree_data 38 | except KeyError: 39 | results = serializer_data 40 | return results 41 | -------------------------------------------------------------------------------- /apps/application/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-31 14:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Diagnosis', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(blank=True, max_length=64, null=True, verbose_name='应用名称')), 19 | ('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='应用IP')), 20 | ('user_name', models.CharField(blank=True, max_length=32, null=True, verbose_name='安装用户')), 21 | ('is_active', models.BooleanField(blank=True, default=0, null=True, verbose_name='是否在线')), 22 | ], 23 | options={ 24 | 'verbose_name': '应用诊断', 25 | 'db_table': 'diagnosis', 26 | 'ordering': ['-id'], 27 | 'db_tablespace': 'diagnosis', 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /apps/rbac/urls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : urls.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | from django.urls import path, include 18 | from rest_framework.routers import SimpleRouter 19 | 20 | from apps.rbac.views import menu, permission, role 21 | 22 | app_name = 'rbac' 23 | 24 | router = SimpleRouter() 25 | router.register(r'roles', role.RoleView, basename='roles') # 权限管理 26 | router.register(r'permissions', permission.PermissionView, basename='permissions') # 权限管理 27 | router.register(r'menu', menu.MenuView, basename='menu') # 菜单管理 28 | 29 | 30 | urlpatterns = [ 31 | path('menu/tree/', menu.MenuTreeView.as_view()), 32 | path('init_permission/', permission.InitPermission.as_view()), 33 | path('paths/', permission.PermissionPath.as_view()), 34 | path('permissions/tree/', permission.PermissionTree.as_view()), 35 | path('permissions/all/', permission.PermissionAll.as_view()), 36 | path('', include(router.urls)), 37 | 38 | ] -------------------------------------------------------------------------------- /controller/ansible/utils.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : utils.py.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/13 14 | # @Desc : 15 | """ 16 | 17 | # import os 18 | # import uuid 19 | # import django 20 | # from django.conf import settings 21 | # django.setup() 22 | # 23 | # def get_ansible_task_log_path(task_id): 24 | # return get_task_log_path(settings.LOG_PATH, task_id, level=3) 25 | # 26 | # 27 | # def get_task_log_path(base_path, task_id, level=2): 28 | # task_id = str(task_id) 29 | # try: 30 | # uuid.UUID(task_id) 31 | # except: 32 | # BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 33 | # PROJECT_DIR = os.path.dirname(BASE_DIR) 34 | # return os.path.join(PROJECT_DIR, 'data', 'caution.txt') 35 | # 36 | # rel_path = os.path.join(*task_id[:level], task_id + '.log') 37 | # path = os.path.join(base_path, rel_path) 38 | # os.makedirs(os.path.dirname(path), exist_ok=True) 39 | # return path 40 | # 41 | # 42 | # 43 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ####### 扫码 2 | DING_QR_CODE_APP_ID = '' 3 | DING_QR_CODE_APP_SECRET = '' 4 | 5 | ###### 应用 6 | DING_AGENT_ID = 7 | DING_APP_KEY = '' 8 | DING_APP_SECRET = '' 9 | 10 | # 钉钉API接口 11 | DING_REQUEST_URL = 'http://dingtalk.com' 12 | # 钉钉扫码登录用户邮箱后缀 13 | DING_LOGIN_EMAIL_SUFFIX = 'pigs.com' 14 | 15 | MONGO_HOST = '192.168.1.34' 16 | MONGO_PORT = 27017 17 | 18 | # -------- Settings Start ---------------------------- 19 | DB_HOST = '192.168.1.96' 20 | DB_PORT = 3306 21 | DB_USER = 'ops' 22 | DB_PASS = '123456789' 23 | DB_TABLE = 'ops' 24 | 25 | CACHES_REDIS_HOST = '192.168.1.234' 26 | CACHES_REDIS_PORT = 6379 27 | CACHES_REDIS_DB = 8 28 | CACHES_REDIS_PASS = '123456789' 29 | 30 | EMAIL_USE_SSL = True 31 | EMAIL_HOST = 'smtp.exmail.qq.com' 32 | EMAIL_PORT = 465 33 | EMAIL_HOST_USER = 'system@huijia.city' 34 | EMAIL_HOST_PASSWORD = '123456789' 35 | EMAIL_FROM = '运维平台' 36 | 37 | CELERY_BROKER_REDIS_HOST = 'redis.com' 38 | CELERY_BROKER_REDIS_PORT = 6379 39 | CELERY_BROKER_REDIS_DB = 4 40 | CELERY_BROKER_REDIS_PASS = '123456' 41 | 42 | CELERY_BACKEND_REDIS_HOST = 'redis.com' 43 | CELERY_BACKEND_REDIS_PORT = 6379 44 | CELERY_BACKEND_REDIS_DB = 5 45 | CELERY_BACKEND_REDIS_PASS = '123456' 46 | # -------- Settings End ------------------------------ 47 | 48 | # api_metrics_url 49 | API_METRICS_URL = 'http://192.168.1.74:9988' 50 | API_METRICS_TOKEN = '123456' 51 | 52 | # 找回密码url 53 | DOMAIN_URL = 'http://192.168.1.35' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aliyun-python-sdk-alidns==2.6.29 2 | aliyun-python-sdk-core==2.13.33 3 | aliyun-python-sdk-domain==3.14.5 4 | aliyun-python-sdk-ecs==4.23.11 5 | amqp==2.6.1 6 | asgiref==3.3.1 7 | billiard==3.6.4.0 8 | cachetools==4.2.1 9 | celery==4.4.2 10 | certifi==2020.12.5 11 | cffi==1.14.5 12 | chardet==4.0.0 13 | cryptography==3.4.7 14 | Django==3.1.7 15 | django-celery-beat==2.2.0 16 | django-celery-results==2.0.1 17 | django-cors-headers==3.7.0 18 | django-filter==2.4.0 19 | django-log-request-id==1.6.0 20 | django-timezone-field==4.1.2 21 | djangorestframework==3.12.4 22 | djangorestframework-jwt==1.11.0 23 | dnspython==1.16.0 24 | et-xmlfile==1.0.1 25 | eventlet==0.30.2 26 | google-auth==1.28.1 27 | greenlet==1.0.0 28 | idna==2.10 29 | jdcal==1.4.1 30 | jmespath==0.10.0 31 | kombu==4.6.11 32 | kubernetes==12.0.1 33 | oauthlib==3.1.0 34 | openpyxl==3.0.6 35 | pyasn1==0.4.8 36 | pyasn1-modules==0.2.8 37 | pycparser==2.20 38 | PyJWT==1.7.1 39 | PyMySQL==1.0.2 40 | python-crontab==2.5.1 41 | python-dateutil==2.8.1 42 | python-decouple==3.4 43 | pytz==2021.1 44 | PyYAML==5.4.1 45 | redis==3.5.3 46 | requests==2.25.1 47 | requests-oauthlib==1.3.0 48 | rsa==4.7.2 49 | six==1.15.0 50 | sqlparse==0.4.1 51 | urllib3==1.26.4 52 | vine==1.3.0 53 | websocket-client==0.58.0 54 | Pint==0.17 55 | channels==3.0.3 56 | ansible==2.8.8 57 | paramiko==2.7.2 58 | pymongo==3.11.4 59 | django-redis==4.12.1 60 | pywinrm>=0.2.2 61 | demjson==2.2.4 62 | channels-redis==3.2.0 -------------------------------------------------------------------------------- /controller/ansible/test_win.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : test_win.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/20 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework.views import APIView 18 | 19 | from controller.ansible.inventory import BaseInventory 20 | from controller.ansible.runner import Options, AdHocRunner 21 | from utils.http_response import APIResponse 22 | 23 | 24 | class AnsibleWin(APIView): 25 | authentication_classes = [] 26 | permission_classes = [] 27 | 28 | def get(self, request, *args, **kwargs): 29 | host_data = [ 30 | {'hostname': 'i-bp10rydhf43t7r07v8wf', 'ip': '123.56.83.237', 31 | 'username': 'administrator', 'password': 'xxx.', 'port': 5985}, 32 | ] 33 | 34 | print(host_data) 35 | inventory = BaseInventory(host_data) 36 | Options.connection = 'winrm' 37 | runner = AdHocRunner(inventory, options=Options) 38 | tasks = [ 39 | {"action": {"module": "win_command", "args": r'powershell C:\Users\Administrator\Desktop\test.ps1'}, "name": "run_whoami"}, 40 | ] 41 | 42 | ret = runner.run(tasks, "all", 111111133333333333333334) 43 | print(ret) 44 | # print(ret.results_summary) 45 | # print(ret.results_raw) 46 | return APIResponse(data=ret.results_raw) 47 | -------------------------------------------------------------------------------- /apps/account/urls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | @Time: 2020/9/27 11:28 6 | @Author: yu.jiang 7 | @License: Apache License 8 | @File: user.py 9 | 10 | """ 11 | 12 | from django.conf.urls import url 13 | 14 | from apps.account.views.dingtalk import DingLogin, DingCallBack 15 | from apps.account.views.user import UserLoginView, CheckEmailExistView, SendCodeView, CheckCodeView, ResetPwdView, \ 16 | ChangePwdView, AccountInfoView, UserRoleView 17 | 18 | app_name = 'account' 19 | 20 | urlpatterns = [ 21 | url(r'^(?P[v1|v2]+)/user/login$', UserLoginView.as_view(), name='login'), 22 | url(r'^(?P[v1|v2]+)/user/dingding$', DingLogin.as_view(), name='ding_login'), 23 | url(r'^(?P[v1|v2]+)/user/dingding/callback$', DingCallBack.as_view(), name='ding_callback'), 24 | url(r'^(?P[v1|v2]+)/user/check_email_exist$', CheckEmailExistView.as_view(), name='check_email_exists'), 25 | url(r'^(?P[v1|v2]+)/user/password/send_code$', SendCodeView.as_view(), name='send_code'), 26 | url(r'^(?P[v1|v2]+)/user/password/check_code$', CheckCodeView.as_view(), name='check_code'), 27 | url(r'^(?P[v1|v2]+)/user/password/reset_pwd$', ResetPwdView.as_view(), name='reset_pwd'), 28 | url(r'^(?P[v1|v2]+)/user/password/change$', ChangePwdView.as_view(), name='change_pwd'), 29 | url(r'^(?P[v1|v2]+)/users$', AccountInfoView.as_view(), name='account_info'), 30 | url(r'^(?P[v1|v2]+)/user/role$', UserRoleView.as_view(), name='user_role'), 31 | 32 | ] -------------------------------------------------------------------------------- /apps/cmdb/serializers/server.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/21 0021 下午 12:27 11 | @Author: micheng. 12 | @File: server.py 13 | """ 14 | 15 | from rest_framework import serializers 16 | 17 | from apps.cmdb.models import Server, Disk 18 | 19 | 20 | class ServerDiskSerializer(serializers.ModelSerializer): 21 | class Meta: 22 | model = Disk 23 | fields = "__all__" 24 | 25 | 26 | class ServerSerializer(serializers.ModelSerializer): 27 | """服务器序列化类""" 28 | # 反向引入disk 29 | disk = ServerDiskSerializer(many=True, read_only=True) 30 | 31 | class Meta: 32 | model = Server 33 | fields = "__all__" 34 | 35 | def to_representation(self, instance): 36 | ret = super(ServerSerializer, self).to_representation(instance) 37 | 38 | # ret.update({ 39 | # 'create_at': instance.create_at.strftime('%Y-%m-%d %H:%M:%S') 40 | # }) 41 | 42 | # ret["public_ip"] = "***.**.**.***" 43 | 44 | host = 'https://ecs.console.aliyun.com' 45 | ret['web_url'] = f'{host}/#/server/{instance.instance_id}/detail?regionId={instance.region_id}' 46 | return ret 47 | 48 | 49 | class ExecuteTaskServerSerializer(serializers.ModelSerializer): 50 | """执行任务 服务器序列化类""" 51 | 52 | class Meta: 53 | model = Server 54 | fields = ["instance_id", "status", "private_ip", "hostname", "os_type"] -------------------------------------------------------------------------------- /apps/k8s/serializers/node.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : node.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/4/4 14 | # @Desc : 15 | """ 16 | 17 | from rest_framework import serializers 18 | 19 | 20 | class NodeSerializers(serializers.Serializer): 21 | """ 22 | Node序列化 23 | """ 24 | host = serializers.IPAddressField() 25 | hostname = serializers.CharField() 26 | name = serializers.CharField() 27 | labels = serializers.JSONField() 28 | allocatable = serializers.JSONField() 29 | capacity = serializers.JSONField() 30 | status = serializers.CharField() 31 | unschedule = serializers.CharField() 32 | allocated_pods = serializers.IntegerField() 33 | node_info = serializers.JSONField() 34 | creation_timestamp = serializers.DateTimeField() 35 | conditions = serializers.JSONField() 36 | resouces = serializers.JSONField() 37 | 38 | # node_all_label = serializers.JSONField() 39 | # cpu_alloc = serializers.CharField() 40 | # cpu_req = serializers.CharField() 41 | # cpu_lmt = serializers.CharField() 42 | # cpu_req_per = serializers.CharField() 43 | # cpu_lmt_per = serializers.CharField() 44 | 45 | # mem_alloc = serializers.CharField() 46 | # mem_req = serializers.CharField() 47 | # mem_lmt = serializers.CharField() 48 | # mem_req_per = serializers.CharField() 49 | # mem_lmt_per = serializers.CharField() 50 | -------------------------------------------------------------------------------- /utils/prpcrypt.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/30 0030 下午 4:21 11 | @Author: micheng. 12 | @File: prpcrypt.py 13 | """ 14 | 15 | from Crypto.Cipher import AES 16 | from binascii import b2a_hex, a2b_hex 17 | 18 | 19 | class Encryption(): 20 | 21 | def __init__(self): 22 | """ 23 | 用于对重要数据进行加密, 密钥key长度必须为: 16位(AES-128) 24位(AES-192) 32位(AES-256) Bytes长度 24 | """ 25 | self.key = "lqpctBm5crSbb&7t" 26 | self.mode = AES.MODE_CBC 27 | 28 | def encrypt(self, text): 29 | cryptor = AES.new(self.key, self.mode, b'0000000000000000') 30 | 31 | length = 16 32 | count = len(text) 33 | if count < length: 34 | add = (length - count) 35 | text = text + ('\0' * add) 36 | 37 | elif count > length: 38 | add = (length - (count % length)) 39 | text = text + ('\0' * add) 40 | 41 | self.ciphertext = cryptor.encrypt(text) 42 | # 因为AES加密时候得到的字符串不一定是ascii字符集的,输出到终端或者保存时候可能存在问题 43 | # 所以这里统一把加密后的字符串转化为16进制字符串 44 | return b2a_hex(self.ciphertext) 45 | 46 | # 解密后,去掉补足的空格用strip() 去掉 47 | def decrypt(self, text): 48 | cryptor = AES.new(self.key, self.mode, b'0000000000000000') 49 | plain_text = cryptor.decrypt(a2b_hex(text)) 50 | return plain_text.rstrip('\0') 51 | 52 | 53 | if __name__ == '__main__': 54 | pc = Encryption() # 初始化密钥 55 | import sys 56 | 57 | e = pc.encrypt(sys.argv[1]) # 加密 58 | d = pc.decrypt(e) # 解密 59 | print("加密:", e) 60 | -------------------------------------------------------------------------------- /apps/cmdb/admin.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | from django.contrib import admin 9 | from apps.cmdb.models import CloudAK, Server, Disk, EcsAuthSSH 10 | 11 | 12 | # Register your models here. 13 | @admin.register(CloudAK) 14 | class CloudAKAdmin(admin.ModelAdmin): 15 | list_display = ['access_key', 'access_secret', 'active'] 16 | list_display_links = ['access_key'] 17 | 18 | 19 | @admin.register(Server) 20 | class ServerAdmin(admin.ModelAdmin): 21 | list_display = ['hostname', 'zone_id', 'private_ip', 'public_ip', 'cpu', 'memory', 'network_type', 'os_name', 22 | 'instance_charge_type', 'status', 'expired_time'] 23 | list_display_links = ['hostname', 'public_ip'] 24 | list_filter = ['network_type', 'os_type', 'zone_id', 'instance_charge_type', 'status'] 25 | search_fields = ['instance_id', 'hostname', 'private_ip', 'public_ip', 'mac_address'] 26 | 27 | 28 | @admin.register(Disk) 29 | class DiskAdmin(admin.ModelAdmin): 30 | list_display = ['instance_id', 'disk_id', 'category', 'encrypted', 'expired_time', 'disk_charge_type', 'size', 'status' 31 | , 'type', 'portable', 'zone_id'] 32 | list_display_links = ['disk_id'] 33 | list_filter = ['category', 'status', 'type', 'encrypted', 'delete_with_instance', 'disk_charge_type'] 34 | search_fields = ['instance__instance_id', 'disk_id', 'serial_number', 'instance__hostname'] 35 | 36 | 37 | @admin.register(EcsAuthSSH) 38 | class EcsAuthSSHAdmin(admin.ModelAdmin): 39 | list_display = ['type', 'username', 'password', 'key', 'port', 'server_type'] 40 | list_display_links = ['type'] -------------------------------------------------------------------------------- /apps/cmdb/views/deploy_chart.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : deploy_chart.py.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/31 14 | # @Desc : 15 | """ 16 | 17 | import traceback 18 | import datetime as dt 19 | import logging 20 | 21 | from django.http import JsonResponse 22 | from rest_framework.views import APIView 23 | 24 | from apps.cmdb.models import Server, AnsibleExecHistory 25 | from utils.authorization import MyAuthentication 26 | from utils.http_response import APIResponse 27 | from utils.permissions import MyPermission 28 | 29 | logger = logging.getLogger('default') 30 | 31 | 32 | class DashboardChart(APIView): 33 | 34 | authentication_classes = [MyAuthentication] 35 | permission_classes = [MyPermission] 36 | 37 | def get(self, request, *args, **kwargs): 38 | """ 39 | 统计应用、主机、工单、部署单数量 40 | :param request: 41 | :param args: 42 | :param kwargs: 43 | :return: 44 | """ 45 | 46 | try: 47 | server_count = Server.objects.all().count() 48 | job_count = AnsibleExecHistory.objects.all().count() 49 | 50 | count_data = { 51 | "server_count": server_count, 52 | "job_count": job_count, 53 | 54 | } 55 | return APIResponse(data=count_data) 56 | 57 | except Exception as e: 58 | logger.error(str(e)) 59 | logger.error('获取仪表盘统计数据失败, 异常原因: %s' % str(traceback.format_exc())) 60 | return APIResponse(errcode=5003, errmsg="获取数据失败") 61 | 62 | -------------------------------------------------------------------------------- /devops_api/asgi.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | """ 9 | ASGI config for pigs-devops-apps project. 10 | 11 | It exposes the ASGI callable as a module-level variable named ``application``. 12 | 13 | For more information on this file, see 14 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 15 | """ 16 | 17 | import os 18 | 19 | from channels.auth import AuthMiddlewareStack 20 | from channels.routing import ProtocolTypeRouter, URLRouter 21 | from django.conf.urls import url 22 | from django.core.asgi import get_asgi_application 23 | from django.urls import re_path, path 24 | 25 | from consumer.webssh import SSHConsumer 26 | from consumer.ecs_webssh import EcsSSHConsumer 27 | from consumer.application import AppDiagnosisConsumer 28 | from utils.ws_auth import QueryAuthMiddleware, TokenAuthMiddlewareStack 29 | 30 | 31 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devops_api.settings') 32 | # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pigs-devops-apps.settings') 33 | application = ProtocolTypeRouter({ 34 | "http": get_asgi_application(), 35 | # Just HTTP for now. (We can add other protocols later.) 36 | 37 | 'websocket': TokenAuthMiddlewareStack( 38 | URLRouter([ 39 | url(r'^ws/pod/(?P\w+.*)/(?P\w+.*)/(?P\d+.*)/(?P\d+.*)', SSHConsumer.as_asgi()), 40 | url(r'^ws/ecs/webssh/(?P\w+.*)/(?P\d+.*)/(?P\d+.*)', EcsSSHConsumer.as_asgi()), 41 | url(r'^ws/application/diagnosis/(?P\w+.*)$', AppDiagnosisConsumer.as_asgi()), 42 | ]) 43 | ) 44 | 45 | }) 46 | # application = get_asgi_application() 47 | -------------------------------------------------------------------------------- /utils/jwt_token.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : jwt_token.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | import datetime 18 | import logging 19 | 20 | import jwt 21 | from jwt import exceptions 22 | from django.conf import settings 23 | 24 | logger = logging.getLogger('default') 25 | 26 | JWT_SALT = settings.SECRET_KEY 27 | 28 | 29 | def create_token(payload, timeout=20): 30 | """ 31 | :param payload: 例如:{'id':1,'username':'xiaoming'}用户信息 32 | :param timeout: token的过期时间,默认20分钟 33 | :return: token 34 | """ 35 | headers = { 36 | 'typ': 'jwt', 37 | 'alg': 'HS256' 38 | } 39 | logger.info('开始创建Token--->', payload) 40 | payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout) 41 | result = jwt.encode(payload=payload, key=JWT_SALT, algorithm="HS256", headers=headers).decode('utf-8') 42 | return result 43 | 44 | 45 | def parse_payload(token): 46 | """ 47 | 对token进行和发行校验并获取payload 48 | :param token: token 49 | :return: 用户信息 50 | """ 51 | result = {'status': False, 'data': None, 'errmsg': None} 52 | try: 53 | verified_payload = jwt.decode(token, JWT_SALT, True) 54 | result['status'] = True 55 | result['data'] = verified_payload 56 | except exceptions.ExpiredSignatureError: 57 | result['errmsg'] = 'token已失效' 58 | except jwt.DecodeError: 59 | result['errmsg'] = 'token认证失败' 60 | except jwt.InvalidTokenError: 61 | result['errmsg'] = '非法的token' 62 | return result 63 | -------------------------------------------------------------------------------- /controller/kube_pod_exec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | """ 9 | # @File : kube_pod_exec.py 10 | # @Author: 往事随风 11 | # @Email: gujiwork@outlook.com 12 | # @Date : 2021/4/30 13 | # @Desc : 14 | """ 15 | 16 | import json 17 | import logging 18 | 19 | from kubernetes import client, config 20 | from kubernetes.stream import stream 21 | from kubernetes.client import ApiException 22 | 23 | logger = logging.getLogger('default') 24 | 25 | 26 | class KubeApi: 27 | def __init__(self, pod, namespace='default'): 28 | config.load_kube_config() 29 | self.namespace = namespace 30 | self.pod = pod 31 | 32 | def pod_exec(self, cols, rows): 33 | api_instance = client.CoreV1Api() 34 | 35 | exec_command = [ 36 | "/bin/sh", 37 | "-c", 38 | 'export LINES=20; export COLUMNS=100; ' 39 | 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] ' 40 | '&& ([ -x /usr/bin/script ] ' 41 | '&& /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) ' 42 | '|| exec /bin/sh'] 43 | container = "" 44 | conf_stream = stream(api_instance.connect_get_namespaced_pod_exec, 45 | name=self.pod, 46 | namespace=self.namespace, 47 | container=container, 48 | command=exec_command, 49 | stderr=True, stdin=True, 50 | stdout=True, tty=True, 51 | _preload_content=False 52 | ) 53 | 54 | conf_stream.write_channel(4, json.dumps({"Height": int(rows), "Width": int(cols)})) 55 | 56 | return conf_stream 57 | -------------------------------------------------------------------------------- /apps/account/models.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | from django.db import models 9 | from django.contrib.auth.models import AbstractUser 10 | from django.utils import timezone 11 | 12 | from apps.rbac.models import Role 13 | 14 | 15 | class User(AbstractUser): 16 | user_id = models.BigIntegerField(verbose_name='用户ID', unique=True, null=True, blank=True) 17 | mobile = models.CharField(max_length=11, verbose_name='手机号') 18 | name = models.CharField(max_length=32, verbose_name='姓名') 19 | job_number = models.CharField(max_length=32, verbose_name='工号', null=True, blank=True) 20 | position = models.CharField(max_length=64, null=True, verbose_name='职位信息', blank=True) 21 | hire_date = models.DateTimeField(verbose_name='入职时间', null=True) 22 | avatar = models.URLField(verbose_name='用户头像', null=True, blank=True) 23 | sex = models.CharField(max_length=8, verbose_name='性别', choices=(('man', '男'), ('women', '女')), default='man') 24 | roles = models.ManyToManyField(Role, verbose_name='角色', blank=True) 25 | department = models.CharField(max_length=128, verbose_name='部门', null=True, blank=True) 26 | 27 | class Meta: 28 | db_table = 'users' 29 | verbose_name = '用户信息' 30 | verbose_name_plural = verbose_name 31 | ordering = ['-id'] 32 | 33 | def __str__(self): 34 | return self.name 35 | 36 | 37 | class History(models.Model): 38 | user = models.ForeignKey(User, on_delete=models.CASCADE) 39 | ip = models.CharField(max_length=50) 40 | type = models.CharField(max_length=20, default='email', verbose_name='登录类型:邮箱、钉钉') # email, ding 41 | created_at = models.CharField(max_length=20, default=timezone.now()) 42 | 43 | class Meta: 44 | db_table = 'login_history' 45 | ordering = ['-id'] 46 | 47 | 48 | -------------------------------------------------------------------------------- /utils/authorization.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : authorization.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/14 14 | # @Desc : 15 | """ 16 | 17 | import logging 18 | 19 | from rest_framework import status 20 | from rest_framework.authentication import BaseAuthentication 21 | from rest_framework.exceptions import AuthenticationFailed 22 | 23 | from apps.account.models import User 24 | from utils.jwt_token import parse_payload 25 | 26 | logger = logging.getLogger('default') 27 | 28 | 29 | class MyAuthentication(BaseAuthentication): 30 | """ 31 | token认证 32 | """ 33 | 34 | def authenticate(self, request): 35 | 36 | authorization = request.META.get('HTTP_AUTHORIZATION', '') 37 | auth = authorization.split() 38 | if not auth: 39 | raise AuthenticationFailed({'msg': '未获取到Authorization请求头', 'status': 403}) 40 | if auth[0].lower() != 'jwt': 41 | raise AuthenticationFailed({'msg': 'Authorization请求头中认证方式错误', 'status': 403}) 42 | if len(auth) == 1: 43 | raise AuthenticationFailed({'msg': "非法Authorization请求头", 'status': 403}) 44 | elif len(auth) > 2: 45 | raise AuthenticationFailed({'msg': "非法Authorization请求头", 'status': 403}) 46 | token = auth[1] 47 | result = parse_payload(token) 48 | 49 | if not result['status']: 50 | raise AuthenticationFailed(result) 51 | 52 | try: 53 | user = User.objects.get(username=result['data'].get('username')) 54 | except User.DoesNotExist: 55 | raise AuthenticationFailed('User does not exist') 56 | return user, token 57 | 58 | def authenticate_header(self, request): 59 | return status.HTTP_401_UNAUTHORIZED 60 | -------------------------------------------------------------------------------- /doc/v2_init_sql/rbac_menu.sql: -------------------------------------------------------------------------------- 1 | SET FOREIGN_KEY_CHECKS=0; 2 | 3 | -- ---------------------------- 4 | -- Table structure for rbac_menu 5 | -- ---------------------------- 6 | DROP TABLE IF EXISTS `rbac_menu`; 7 | CREATE TABLE `rbac_menu` ( 8 | `id` int(11) NOT NULL AUTO_INCREMENT, 9 | `name` varchar(32) NOT NULL, 10 | `icon` varchar(32) NOT NULL, 11 | `path` varchar(100) NOT NULL, 12 | `is_active` tinyint(1) NOT NULL, 13 | `sort` int(11) NOT NULL, 14 | `pid_id` int(11) DEFAULT NULL, 15 | PRIMARY KEY (`id`), 16 | UNIQUE KEY `name` (`name`), 17 | KEY `rbac_menu_pid_id_a43b3c84_fk_rbac_menu_id` (`pid_id`), 18 | CONSTRAINT `rbac_menu_pid_id_a43b3c84_fk_rbac_menu_id` FOREIGN KEY (`pid_id`) REFERENCES `rbac_menu` (`id`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8; 20 | 21 | -- ---------------------------- 22 | -- Records of rbac_menu 23 | -- ---------------------------- 24 | INSERT INTO `rbac_menu` VALUES ('1', '资产管理', 'cloud-server', '', '1', '1', null); 25 | INSERT INTO `rbac_menu` VALUES ('2', '服务器', '', '/cmdb/server', '1', '11', '1'); 26 | INSERT INTO `rbac_menu` VALUES ('3', '作业配置', 'control', '', '1', '2', null); 27 | INSERT INTO `rbac_menu` VALUES ('4', '执行任务', '', '/task/execute', '1', '21', '3'); 28 | INSERT INTO `rbac_menu` VALUES ('5', '任务模版', '', '/task/template', '1', '22', '3'); 29 | INSERT INTO `rbac_menu` VALUES ('6', '容器管理', 'deployment-unit', '', '1', '3', null); 30 | INSERT INTO `rbac_menu` VALUES ('7', '节点池', '', '/container/nodes', '1', '31', '6'); 31 | INSERT INTO `rbac_menu` VALUES ('8', '工作负载', '', '/container/workload', '1', '32', '6'); 32 | INSERT INTO `rbac_menu` VALUES ('9', '运维工具', 'coffee', '', '1', '4', null); 33 | INSERT INTO `rbac_menu` VALUES ('10', '应用诊断', '', '/application/diagnosis', '1', '41', '9'); 34 | INSERT INTO `rbac_menu` VALUES ('11', '用户管理', 'user', '', '1', '9', null); 35 | INSERT INTO `rbac_menu` VALUES ('12', '用户中心', '', '/user/manage', '1', '91', '11'); 36 | INSERT INTO `rbac_menu` VALUES ('13', '角色列表', '', '/user/roles', '1', '92', '11'); 37 | -------------------------------------------------------------------------------- /utils/code.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/17 0017 下午 6:09 11 | @Author: micheng. 12 | @File: code.py 13 | """ 14 | 15 | import random 16 | import string 17 | import uuid 18 | 19 | 20 | class RandCode(): 21 | @classmethod 22 | def get_code(self): 23 | """ 24 | 发送6位验证码 25 | :return: 26 | """ 27 | code = {} 28 | num = '123456789' 29 | send_message_code = ''.join(random.choice(num) for i in range(6)) 30 | # send_message_code = ''.join(random.choice(string.digits) for i in range(6)) 31 | code["code"] = send_message_code 32 | 33 | return code 34 | 35 | @classmethod 36 | def random_password(pass_len=12, chars=string.ascii_letters + string.digits): 37 | """ 38 | 生成随机密码 39 | :param length: 长度 40 | :param chars: 随机字母 41 | :return: 42 | """ 43 | password = ''.join([random.choice(chars) for i in range(18)]) 44 | return password 45 | 46 | @classmethod 47 | def ticket_number_code(self): 48 | """ 49 | 生成随机工单编号 50 | """ 51 | code = "" 52 | for i in range(8): 53 | add_num = str(random.randrange(0,9)) 54 | add_al = chr(random.randrange(65,91)) 55 | # chr转换为A-Z大写。print(chr(90))#65-90任意生成A-Z 56 | sj = random.choice([add_num,add_al,add_al,add_num]) 57 | # str.lower()转换为小写,为了保证概率,将_add_num写两遍,这样,字母和数字概率一样了 58 | code = "".join([sj,code]) 59 | return code 60 | 61 | @staticmethod 62 | def uuid1_hex(): 63 | """ 64 | return uuid1 hex string 65 | eg: 23f87b528d0f11e696a7f45c89a84eed 66 | """ 67 | return uuid.uuid1().hex 68 | 69 | @classmethod 70 | def uuid4_int(self): 71 | """ 72 | return 28078616856779081225165040310201159828 int 73 | """ 74 | return uuid.uuid4().int -------------------------------------------------------------------------------- /controller/ansible/test_runner.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : test_runner.py.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/12 14 | # @Desc : 15 | """ 16 | 17 | import unittest 18 | import sys 19 | 20 | sys.path.insert(0, "../..") 21 | 22 | from controller.ansible.runner import AdHocRunner, CommandRunner 23 | from controller.ansible.inventory import BaseInventory 24 | 25 | 26 | class TestAdHocRunner(unittest.TestCase): 27 | def setUp(self): 28 | host_data = [ 29 | { 30 | "hostname": "testserver", 31 | "ip": "192.168.1.35", 32 | "port": 22, 33 | "username": "root", 34 | "password": "xxxxxx.", 35 | }, 36 | ] 37 | inventory = BaseInventory(host_data) 38 | self.runner = AdHocRunner(inventory) 39 | 40 | def test_run(self): 41 | tasks = [ 42 | {"action": {"module": "shell", "args": "ls"}, "name": "run_cmd"}, 43 | {"action": {"module": "shell", "args": "whoami"}, "name": "run_whoami"}, 44 | ] 45 | ret = self.runner.run(tasks, "all") 46 | print(ret.results_summary) 47 | print(ret.results_raw) 48 | 49 | 50 | # class TestCommandRunner(unittest.TestCase): 51 | # def setUp(self): 52 | # host_data = [ 53 | # { 54 | # "hostname": "testserver", 55 | # "ip": "192.168.1.35", 56 | # "port": 22, 57 | # "username": "root", 58 | # "password": "xxxxx.", 59 | # }, 60 | # ] 61 | # inventory = BaseInventory(host_data) 62 | # self.runner = CommandRunner(inventory) 63 | # 64 | # def test_execute(self): 65 | # res = self.runner.execute('ls', 'all') 66 | # print(res.results_command) 67 | # print(res.results_raw) 68 | 69 | 70 | if __name__ == "__main__": 71 | unittest.main() -------------------------------------------------------------------------------- /controller/ansible/test_yaml.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : test_cmd.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/12 14 | # @Desc : 15 | """ 16 | 17 | import unittest 18 | import sys 19 | 20 | sys.path.insert(0, "../..") 21 | 22 | from controller.ansible.runner import AdHocRunner, CommandRunner, PlayBookRunner, get_default_options, Options 23 | from controller.ansible.inventory import BaseInventory 24 | 25 | 26 | class TestCmd(object): 27 | def __init__(self): 28 | host_list = [{ 29 | "hostname": "testserver1", 30 | "ip": "192.168.1.35", 31 | "port": 22, 32 | "username": "root", 33 | "password": "xxxxx.", 34 | # "private_key": "/tmp/private_key", 35 | # "become": { 36 | # "method": "sudo", 37 | # "user": "root", 38 | # "pass": None, 39 | # }, 40 | "groups": ["group1", "group2"], 41 | "vars": {"sexy": "yes"}, 42 | }, { 43 | "hostname": "testserver2", 44 | "ip": "192.168.1.36", 45 | "port": 27123, 46 | "username": "root", 47 | "password": "xxxxxxx.", 48 | # "private_key": "/tmp/private_key", 49 | # "become": { 50 | # "method": "su", 51 | # "user": "root", 52 | # "pass": "123", 53 | # }, 54 | "groups": ["group3", "group4"], 55 | "vars": {"love": "yes"}, 56 | }] 57 | print(host_list) 58 | self.inventory = BaseInventory(host_list=host_list) 59 | Options.playbook_path = '/tmp/test.yaml' 60 | Options.passwords = '' 61 | self.runner = PlayBookRunner(inventory=self.inventory, options=Options) 62 | 63 | def test_run(self): 64 | ret = self.runner.run() 65 | import json 66 | print(json.dumps(ret, indent=4)) 67 | # print(ret.results_raw) 68 | 69 | 70 | if __name__ == "__main__": 71 | p = TestCmd() 72 | p.test_run() 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 小飞猪运维平台 2 | 3 | Author: [ 迷城 ] [ 往事随风 ]
4 | 5 | 该项目为devops-api的后端部分, 前端VUE部分请关注 :heart: [devops-web](https://github.com/small-flying-pigs/devops) 6 | https://github.com/small-flying-pigs/devops 7 | 8 | 9 | 10 | 11 | ## Contents 12 | * [介绍](#introduce) 13 | * [预览](#looklike) 14 | * [如何安装](#howtoinstall) 15 | * [自运维功能实现](#whatcando) 16 | 17 | ## 介绍 18 | - 后端:Django 3.0 + REST framework + Celery 19 | - 前端:Vue 3.0 + Ant Design of Vue 20 | 21 | 22 | 23 | ## 预览 24 | ### 登陆界面 25 | 支持钉钉扫码登陆、账号密码登陆两种方式, 钉钉需要在管理后台创建应用 26 | 27 | ![LOGIN](img/login.png)
28 | 29 | ### 资产管理 30 | ![RUN](img/server2.png) 31 | ![RUN](img/server.png) 32 | ![RUN](img/server_ssh.png) 33 | 34 | ### 作业管理 35 | !(img/job.png) 36 | !(img/job2.png) 37 | !(img/send_file.png) 38 | 39 | 40 | ### Kubernetes管理 41 | 42 | Node节点池 43 | 44 | ![RUN](img/node.png) 45 | 46 | Node详情页 47 | ![RUN](img/node_detail1.png) 48 | 49 | ![RUN](img/k8s_detail2.png) 50 | 51 | Deployment 无状态服务 52 | ![RUN](img/workload.png) 53 | 54 | 扩缩容 55 | ![RUN](img/scale.png) 56 | 57 | 容器组 58 | ![RUN](img/container.png) 59 | 60 | ![RUN](img/container2.png) 61 | ![RUN](img/container_logs.png) 62 | ![RUN](img/container_ssh.png) 63 | pod在线编辑 64 | ![RUN](img/yaml_edit.png) 65 | 66 | ### JAVA应用在线诊断 67 | ![RUN](img/app.png) 68 | ![RUN](img/method.png) 69 | 70 | ## 如何安装 71 | devops-api依赖于python3.5以上、诸多python第三方模块、mysql数据库、redis缓存
72 | 以下操作环境已经拥有python3.5、mysql数据库、redis缓存 73 | ```bash 74 | $ cd path/to/project/folder/ 75 | 76 | 77 | #安装python第三方库 78 | $ pip install -r requirements.txt 79 | 80 | #修改.env 配置文件 81 | 将变量替换成自己对应的 82 | 83 | 84 | #连接本地资源并创建数据表结构 85 | $ vim deveops-api/.env # 里面包含了所有连接数据以及定时任务 请填写您需要的数据内容 86 | $ python manage.py makemigrations 87 | $ python manage.py migrate 88 | 89 | 90 | #启动服务 91 | $ python manage.py runserver & 92 | #默认启动在8000端口 你可能需要一个nginx做Web服务器 93 | 94 | ``` 95 | 注意: 请参考doc下面的相关文档, 推荐部署在Linux系统中。 96 | 97 | #### 启动定时任务和异步任务 98 | ``` 99 | celery -A devops beat -l info 100 | 101 | celery worker -A devops --pool=solo -l INFO 102 | ``` 103 | #### 更多功能正在开发中..... 104 | 105 | #### 开发者QQ群: 258130203 106 | -------------------------------------------------------------------------------- /apps/rbac/models.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | from django.db import models 9 | 10 | 11 | class Menu(models.Model): 12 | """ 13 | 菜单 14 | """ 15 | name = models.CharField(verbose_name='菜单名称', max_length=32, unique=True) 16 | icon = models.CharField(verbose_name='图标', max_length=32, blank=True) 17 | path = models.CharField(verbose_name='链接地址', help_text='如果有子菜单,不需要填写该字段', blank=True, max_length=100) 18 | is_active = models.BooleanField(verbose_name='激活状态', default=True) 19 | sort = models.IntegerField(verbose_name='排序标记', blank=True) 20 | pid = models.ForeignKey("self", verbose_name="上级菜单", null=True, blank=True, on_delete=models.SET_NULL) 21 | 22 | class Meta: 23 | verbose_name = '菜单' 24 | verbose_name_plural = verbose_name 25 | ordering = ['sort'] 26 | 27 | def __str__(self): 28 | return self.name 29 | 30 | 31 | class Permission(models.Model): 32 | """ 33 | 权限 34 | """ 35 | name = models.CharField(verbose_name='权限名称', max_length=32, unique=True) 36 | path = models.CharField(verbose_name='含正则的URL', blank=True, max_length=128) 37 | method = models.CharField(verbose_name='方法', max_length=16, default='GET') 38 | pid = models.ForeignKey('self', verbose_name='上级权限', null=True, blank=True, on_delete=models.SET_NULL) 39 | 40 | class Meta: 41 | verbose_name = '权限' 42 | verbose_name_plural = verbose_name 43 | ordering = ['id'] 44 | 45 | def __str__(self): 46 | return self.name 47 | 48 | 49 | class Role(models.Model): 50 | """ 51 | 角色 52 | """ 53 | name = models.CharField(verbose_name='角色名称', max_length=32, unique=True) 54 | permissions = models.ManyToManyField('Permission', verbose_name='权限', blank=True) 55 | menus = models.ManyToManyField('Menu', verbose_name='菜单', blank=True) 56 | desc = models.CharField(verbose_name='描述', max_length=50, blank=True) 57 | 58 | class Meta: 59 | verbose_name = '角色' 60 | verbose_name_plural = verbose_name 61 | ordering = ['id'] 62 | 63 | def __str__(self): 64 | return self.name 65 | -------------------------------------------------------------------------------- /controller/ansible/mongo_logs.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/18 0007 下午 10:26 11 | @Author: micheng. 12 | @File: mongo_logs.py 13 | """ 14 | 15 | import pymongo 16 | from decouple import config 17 | 18 | 19 | class Mongodb(object): 20 | """ 21 | Mongo CURD 22 | """ 23 | 24 | def __init__(self, db='ansible', collection='task_log'): 25 | self.client = pymongo.MongoClient(host=config('MONGO_HOST'), port=int(config('MONGO_PORT'))) 26 | self.db = self.client[db] 27 | self.col = self.db[collection] 28 | 29 | def insert(self, content): 30 | return self.col.insert_one(content) 31 | 32 | # def find_all(self, content=None): result = [] for i in self.col.find({"status.host": "i-bp10rydhf43t7r07v8we", 33 | # "task_id": "14270011787911182284566604763026587502"}): result.append(i) return result 34 | 35 | def filter(self, task_id, status): 36 | result = [] 37 | if task_id and status: 38 | agg = [{"$match": {"task_id": str(task_id)}}, {"$unwind": "$hosts"}, { 39 | "$match": {"hosts.status": status}}, { 40 | "$group": {"_id": "$_id", "task_id": {"$first": "$task_id"}, "hosts": {"$push": "$hosts"}}}] 41 | 42 | job_result = self.col.aggregate(agg) 43 | for res in job_result: 44 | del res['_id'] 45 | result.append(res) 46 | return result 47 | 48 | elif task_id and status == '' or status is None: 49 | job_result = self.col.find({'task_id': str(task_id)}, {"_id": 0}).sort([('time', 1)]) 50 | for res in job_result: 51 | result.append(res) 52 | return result 53 | else: 54 | return False 55 | 56 | def update(self, search_value: dict, update_value: dict) -> dict: 57 | ret = self.col.update(search_value, {"$addToSet": update_value}) 58 | # self.col.update({"status.host": "i-bp10rydhf43t7r07v8we","task_id": 59 | # "14270011787911182284566604763026587502"}, {"$push": {'11': 22}}) 60 | return ret 61 | -------------------------------------------------------------------------------- /apps/rbac/views/menu.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : menu.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | import logging 18 | import traceback 19 | 20 | from rest_framework import status 21 | from rest_framework.response import Response 22 | from rest_framework.views import APIView 23 | from rest_framework.viewsets import ModelViewSet 24 | 25 | from apps.rbac.models import Menu 26 | from apps.rbac.serializers import menu_serializers 27 | from apps.account.models import User 28 | from utils.pagination import MenuPagination 29 | from utils.tree import tree_filter 30 | from utils.authorization import MyAuthentication 31 | from utils.permissions import MyPermission 32 | 33 | logger = logging.getLogger('default') 34 | 35 | 36 | class MenuView(ModelViewSet): 37 | """ 38 | 菜单管理 39 | """ 40 | pagination_class = MenuPagination 41 | permission_classes = [] 42 | queryset = Menu.objects.all() 43 | serializer_class = menu_serializers.MenuSerializer 44 | 45 | 46 | class MenuTreeView(APIView): 47 | authentication_classes = [MyAuthentication] 48 | permission_classes = [MyPermission, ] 49 | 50 | """ 51 | 菜单树 52 | """ 53 | 54 | def get(self, request, *args, **kwargs): 55 | try: 56 | username = request.user.username 57 | except Exception as e: 58 | logger.error('获取用户信息出错了:%s' % str(traceback.format_exc()), e) 59 | username = request.user['data'] 60 | try: 61 | 62 | user_obj = User.objects.filter(username=username).first() 63 | menus_list = user_obj.roles.all().values('menus') 64 | except Exception as e: 65 | logger.error('无法解析菜单: %s' % e) 66 | return Response(status=status.HTTP_403_FORBIDDEN) 67 | menus = [i.get('menus') for i in menus_list] 68 | queryset = Menu.objects.all() 69 | # 序列化菜单 70 | serializer = menu_serializers.MenuSerializer(queryset, many=True) 71 | # 树形菜单 72 | results = tree_filter(serializer.data, menus) 73 | return Response(results) 74 | -------------------------------------------------------------------------------- /controller/ansible/test_inventory.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : test_inventory.py.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/12 14 | # @Desc : 15 | """ 16 | 17 | import sys 18 | import unittest 19 | 20 | from controller.ansible.runner import AdHocRunner 21 | from controller.ansible.inventory import BaseInventory 22 | 23 | sys.path.insert(0, '../..') 24 | 25 | 26 | class TestJMSInventory(unittest.TestCase): 27 | def setUp(self): 28 | host_list = [{ 29 | "hostname": "testserver1", 30 | "ip": "192.168.1.35", 31 | "port": 22, 32 | "username": "root", 33 | "password": "xxxxxx.", 34 | "private_key": "/tmp/private_key", 35 | "become": { 36 | "method": "sudo", 37 | "user": "root", 38 | "pass": None, 39 | }, 40 | "groups": ["group1", "group2"], 41 | "vars": {"sexy": "yes"}, 42 | }, { 43 | "hostname": "testserver2", 44 | "ip": "192.168.1.36", 45 | "port": 27123, 46 | "username": "root", 47 | "password": "xxxxxx...", 48 | "private_key": "/tmp/private_key", 49 | "become": { 50 | "method": "su", 51 | "user": "root", 52 | "pass": "123", 53 | }, 54 | "groups": ["group3", "group4"], 55 | "vars": {"love": "yes"}, 56 | }] 57 | 58 | self.inventory = BaseInventory(host_list=host_list) 59 | self.runner = AdHocRunner(self.inventory) 60 | 61 | def test_hosts(self): 62 | print("#"*10 + "Hosts" + "#"*10) 63 | for host in self.inventory.hosts: 64 | print(host) 65 | 66 | def test_groups(self): 67 | print("#" * 10 + "Groups" + "#" * 10) 68 | for group in self.inventory.groups: 69 | print(group) 70 | 71 | def test_group_all(self): 72 | print("#" * 10 + "all group hosts" + "#" * 10) 73 | group = self.inventory.get_group('all') 74 | print(group.hosts) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() -------------------------------------------------------------------------------- /apps/k8s/urls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : urls.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/7 14 | # @Desc : 15 | """ 16 | 17 | from django.conf.urls import url 18 | from django.urls import path 19 | 20 | from apps.k8s.views.deployment import DeploymentViews, DetailDeploymentViews, DeploymentHistoryVersionViews 21 | from apps.k8s.views.logs import LogsView 22 | from apps.k8s.views.namespace import NamespaceView 23 | from apps.k8s.views.node import NodeView, DarinNodeAndPod, NodeDetailView, NodeEventsView, NodeMetrics 24 | from apps.k8s.views.pod import PodFromNodeView, DetailPodView, PodViews 25 | from apps.k8s.views.events import EventsView, DeploymentEventsView 26 | from apps.k8s.views.scale import DeploymentScaleViews 27 | 28 | app_name = 'k8s' 29 | 30 | urlpatterns = [ 31 | url(r'^(?P[v1|v2]+)/drain/nodes$', DarinNodeAndPod.as_view(), name='node_drain'), 32 | url(r'^(?P[v1|v2]+)/nodes$', NodeView.as_view(), name='nodes'), 33 | url(r'^(?P[v1|v2]+)/detail/node$', NodeDetailView.as_view(), name='detail_node'), 34 | url(r'^(?P[v1|v2]+)/events/node$', NodeEventsView.as_view(), name='event_node'), 35 | url(r'^(?P[v1|v2]+)/event/deployment$', DeploymentEventsView.as_view(), name='event_node'), 36 | url(r'^(?P[v1|v2]+)/metrics/node$', NodeMetrics.as_view(), name='metrics_node'), 37 | url(r'^(?P[v1|v2]+)/pods/node$', PodFromNodeView.as_view(), name='pod_to_node'), 38 | url(r'^(?P[v1|v2]+)/pods/list$', PodViews.as_view(), name='pods'), 39 | url(r'^(?P[v1|v2]+)/event/pod$', EventsView.as_view(), name='event_pod'), 40 | url(r'^(?P[v1|v2]+)/namespaces$', NamespaceView.as_view(), name='namespaces'), 41 | url(r'^(?P[v1|v2]+)/pod$', DetailPodView.as_view(), name='pod'), 42 | url(r'^(?P[v1|v2]+)/logs$', LogsView.as_view(), name='logs'), 43 | url(r'^(?P[v1|v2]+)/deployments$', DeploymentViews.as_view(), name='deployments'), 44 | url(r'^(?P[v1|v2]+)/deployment/detail$', DetailDeploymentViews.as_view(), name='deployment'), 45 | url(r'^(?P[v1|v2]+)/deployment/scale$', DeploymentScaleViews.as_view(), name='scale'), 46 | url(r'^(?P[v1|v2]+)/deployment/history$', DeploymentHistoryVersionViews.as_view(), name='history_version'), 47 | ] -------------------------------------------------------------------------------- /consumer/application.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/28 0028 上午 10:15 11 | @Author: micheng. 12 | @File: application.py 13 | """ 14 | 15 | import json 16 | import logging 17 | 18 | from channels.generic.websocket import WebsocketConsumer, JsonWebsocketConsumer, AsyncWebsocketConsumer 19 | from asgiref.sync import async_to_sync 20 | 21 | from tasks import application 22 | from tasks.application import app_diagnosis 23 | 24 | logger = logging.getLogger('default') 25 | consumer_list = [] 26 | 27 | 28 | class AppDiagnosisConsumer(AsyncWebsocketConsumer): 29 | """ 30 | 应用诊断 31 | """ 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | self.agent_id = None 36 | self.type = None 37 | self.command = None 38 | self.room_group_name = "DEFAULT_ROOM" 39 | 40 | async def connect(self): 41 | print(self.scope) 42 | logger.info(f"websocket携带参数: {self.scope}") 43 | self.agent_id = self.scope['url_route']['kwargs']['agentId'] 44 | self.room_group_name = self.agent_id 45 | 46 | # Join room group 47 | await self.channel_layer.group_add( 48 | self.room_group_name, 49 | self.channel_name 50 | ) 51 | 52 | await self.accept() 53 | 54 | async def disconnect(self, close_code): 55 | # Leave room group 56 | await self.channel_layer.group_discard( 57 | self.room_group_name, 58 | self.channel_name 59 | ) 60 | 61 | # Receive message from WebSocket 62 | async def receive(self, text_data): 63 | text_data_json = json.loads(text_data) 64 | print(f'接收到的websocket数据: {text_data_json}') 65 | 66 | getattr(application, 'app_diagnosis').delay(self.channel_name, *text_data_json) 67 | 68 | # Send message to room group 69 | await self.channel_layer.group_send( 70 | self.room_group_name, 71 | { 72 | 'type': 'arthas', 73 | 'message': text_data_json 74 | } 75 | ) 76 | 77 | # Receive message from room group 78 | async def arthas(self, event): 79 | print(event) 80 | 81 | # Send message to WebSocket 82 | await self.send(text_data=json.dumps({ 83 | 'data': event 84 | })) 85 | -------------------------------------------------------------------------------- /controller/ansible/display.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/7 0007 下午 4:03 11 | @Author: micheng. 12 | @File: display.py 13 | """ 14 | import errno 15 | import sys 16 | import os 17 | 18 | from ansible.utils.display import Display 19 | from ansible.utils.color import stringc 20 | from ansible.utils.singleton import Singleton 21 | 22 | # from controller.ansible.utils import get_ansible_task_log_path 23 | 24 | 25 | class UnSingleton(Singleton): 26 | def __init__(cls, name, bases, dct): 27 | type.__init__(cls, name, bases, dct) 28 | 29 | def __call__(cls, *args, **kwargs): 30 | return type.__call__(cls, *args, **kwargs) 31 | 32 | 33 | class AdHocDisplay(Display, metaclass=UnSingleton): 34 | def __init__(self, execution_id, verbosity=0): 35 | super().__init__(verbosity=verbosity) 36 | if execution_id: 37 | # ("execution_id", execution_id) 38 | pass 39 | # log_path = get_ansible_task_log_path(execution_id) 40 | else: 41 | log_path = os.devnull 42 | # self.log_file = open(log_path, mode='a') 43 | 44 | def close(self): 45 | print("Close") 46 | # self.log_file.close() 47 | 48 | def set_cowsay_info(self): 49 | # 中断 cowsay 的测试,会频繁开启子进程 50 | return 51 | 52 | def _write_to_screen(self, msg, stderr): 53 | if not stderr: 54 | screen = sys.stdout 55 | else: 56 | screen = sys.stderr 57 | 58 | screen.write(msg) 59 | 60 | try: 61 | screen.flush() 62 | except IOError as e: 63 | # Ignore EPIPE in case fileobj has been prematurely closed, eg. 64 | # when piping to "head -n1" 65 | if e.errno != errno.EPIPE: 66 | raise 67 | 68 | def _write_to_log_file(self, msg): 69 | # 这里先不 flush,log 文件不需要那么及时。 70 | print("msg", msg) 71 | # self.log_file.write(msg) 72 | 73 | def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False, newline=True): 74 | if color: 75 | msg = stringc(msg, color) 76 | 77 | if not msg.endswith(u'\n'): 78 | msg2 = msg + u'\n' 79 | else: 80 | msg2 = msg 81 | 82 | if log_only: 83 | print("msg2", msg2) 84 | # self._write_to_log_file(msg2) 85 | else: 86 | self._write_to_screen(msg2, stderr) -------------------------------------------------------------------------------- /apps/k8s/views/events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | # @File : events.py 9 | # @Author: 往事随风 10 | # @Email: gujiwork@outlook.com 11 | # @Date : 2021/4/28 12 | # @Desc : 13 | 14 | import logging 15 | import traceback 16 | 17 | from kubernetes.client import ApiException 18 | from rest_framework.views import APIView 19 | from kubernetes import client, config 20 | 21 | from utils.authorization import MyAuthentication 22 | from utils.http_response import APIResponse 23 | from utils.permissions import MyPermission 24 | 25 | config.load_kube_config() 26 | logger = logging.getLogger('default') 27 | app_v2 = client.CoreV1Api() 28 | 29 | 30 | class EventsView(APIView): 31 | authentication_classes = [MyAuthentication] 32 | permission_classes = [MyPermission, ] 33 | 34 | def get(self, request, *args, **kwargs): 35 | """ 36 | 获取Pod、Node事件信息 37 | """ 38 | data = request.query_params.dict() 39 | # 如果传递的是Node名称则获取Node的事件, 如果传递的是Pod名称则获取Pod的事件 40 | name = data.get('name') 41 | if name: 42 | try: 43 | res = app_v2.list_event_for_all_namespaces( 44 | field_selector="involvedObject.name={}".format(name)) 45 | return APIResponse(data=res.to_dict()) 46 | except ApiException as e: 47 | logger.error(e, str(traceback.format_exc())) 48 | return APIResponse(errcode=e.status, errmsg=e.body) 49 | except Exception as e: 50 | logger.error(e, str(traceback.format_exc())) 51 | return APIResponse(errcode=9999, errmsg='获取事件信息失败') 52 | else: 53 | return APIResponse(errcode=404, errmsg='传递的参数不能为空') 54 | 55 | 56 | class DeploymentEventsView(APIView): 57 | authentication_classes = [MyAuthentication] 58 | permission_classes = [MyPermission, ] 59 | 60 | def get(self, request, *args, **kwargs): 61 | """ 62 | 获取Deployment事件信息 63 | """ 64 | data = request.query_params.dict() 65 | name = data.get('name') 66 | namespace = data.get('namespace') 67 | try: 68 | app = client.CoreV1Api() 69 | res = app.list_event_for_all_namespaces( 70 | field_selector=f"involvedObject.kind=Deployment,involvedObject.name={name},involvedObject.namespace={namespace}" 71 | ) 72 | return APIResponse(data=res.to_dict()) 73 | except Exception as e: 74 | logger.exception(e) 75 | return APIResponse(errcode=500, errmsg='获取deployment事件失败') 76 | -------------------------------------------------------------------------------- /utils/ws_auth.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : ws_auth.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/30 14 | # @Desc : 15 | """ 16 | 17 | import os 18 | import traceback 19 | from urllib.parse import parse_qs 20 | 21 | from channels.auth import AuthMiddlewareStack 22 | from channels.db import database_sync_to_async 23 | from django.contrib.auth.models import AnonymousUser 24 | from django.contrib.auth import get_user_model 25 | from rest_framework.exceptions import AuthenticationFailed 26 | 27 | from apps.account.models import User 28 | from utils.jwt_token import parse_payload 29 | 30 | os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" 31 | 32 | # Users = get_user_model() 33 | 34 | 35 | @database_sync_to_async 36 | def get_user(username): 37 | try: 38 | return User.objects.get(username=username) 39 | except User.DoesNotExist: 40 | return AnonymousUser() 41 | 42 | 43 | class QueryAuthMiddleware: 44 | """ 45 | Custom middleware (insecure) that takes user IDs from the query string. 46 | """ 47 | 48 | def __init__(self, app): 49 | # Store the ASGI application we were passed 50 | self.app = app 51 | 52 | async def __call__(self, scope, receive, send): 53 | # Look up user from query string (you should also do things like 54 | # checking if it is a valid user ID, or if scope["user"] is already 55 | # populated). 56 | token = parse_qs(scope["query_string"].decode("utf8"))["token"][0] 57 | 58 | # Try to authenticate the user 59 | try: 60 | # This will automatically validate the token and raise an error if token is invalid 61 | result = parse_payload(token) 62 | if not result['status']: 63 | raise AuthenticationFailed(result) 64 | 65 | username = result['data'].get('username') 66 | 67 | scope['user'] = await get_user(username) 68 | except Exception: 69 | print(traceback.format_exc()) 70 | raise AuthenticationFailed('认证失败!') 71 | 72 | # Will return a dictionary like - 73 | # { 74 | # "token_type": "access", 75 | # "exp": 1568770772, 76 | # "jti": "5c15e80d65b04c20ad34d77b6703251b", 77 | # "user_id": 6 78 | # } 79 | # Get the user using ID 80 | return await self.app(scope, receive, send) 81 | 82 | 83 | TokenAuthMiddlewareStack = lambda inner: QueryAuthMiddleware(AuthMiddlewareStack(inner)) 84 | -------------------------------------------------------------------------------- /apps/cmdb/urls.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/13 0013 下午 5:14 11 | @Author: micheng. 12 | @File: urls.py 13 | """ 14 | 15 | from django.conf.urls import url 16 | # from django.urls import path 17 | 18 | from apps.cmdb.views.ansible_tasks import ExecuteTaskView, ExecuteTaskServerList, SearchHostView, TasksExecList, \ 19 | UploadFile, SendFileView, SendFileList, AnsibleTemplateView, AnsibleTemplateReadView, TemplateSearch, \ 20 | SendFileSearch, CommandSearch 21 | from apps.cmdb.views.deploy_chart import DashboardChart 22 | 23 | from apps.cmdb.views.domain import DomainView 24 | from apps.cmdb.views.server import ServerView, ServerDetailView, ServerSearchView 25 | 26 | urlpatterns = [ 27 | url(r'^(?P[v1|v2]+)/server$', ServerView.as_view(), name='ecs'), 28 | url(r'^(?P[v1|v2]+)/server/search$', ServerSearchView.as_view()), 29 | url(r'^(?P[v1|v2]+)/server/(?P\w+.*)$', ServerDetailView.as_view()), 30 | url(r'^(?P[v1|v2]+)/domain$', DomainView.as_view(), name='domain'), 31 | # ansible task 32 | url(r'^(?P[v1|v2]+)/ansible/server$', ExecuteTaskServerList.as_view(), name='task_server'), 33 | url(r'^(?P[v1|v2]+)/ansible/tasks/list$', TasksExecList.as_view(), name='tasks_list'), 34 | url(r'^(?P[v1|v2]+)/ansible/execute$', ExecuteTaskView.as_view(), name='execute_task'), 35 | url(r'^(?P[v1|v2]+)/ansible/execute/search$', CommandSearch.as_view(), name='command_search'), 36 | url(r'^(?P[v1|v2]+)/ansible/searchHost$', SearchHostView.as_view(), name='search_server'), 37 | url(r'^(?P[v1|v2]+)/ansible/sendfile$', SendFileView.as_view(), name='send_file'), 38 | url(r'^(?P[v1|v2]+)/ansible/send_list$', SendFileList.as_view(), name='send_list'), 39 | url(r'^(?P[v1|v2]+)/ansible/sendfile/search$', SendFileSearch.as_view(), name='send_file_search'), 40 | url(r'^(?P[v1|v2]+)/ansible/template/list$', AnsibleTemplateView.as_view(), name='template_list'), 41 | url(r'^(?P[v1|v2]+)/ansible/template/read$', AnsibleTemplateReadView.as_view(), name='template_read'), 42 | url(r'^(?P[v1|v2]+)/ansible/template/search$', TemplateSearch.as_view(), name='template_search'), 43 | url(r'^(?P[v1|v2]+)/uploads$', UploadFile.as_view(), name='upload_file'), 44 | # 仪表盘图表 45 | url(r'^(?P[v1|v2]+)/dashboard/count$', DashboardChart.as_view(), name='dashboard_count'), 46 | # url(r'^(?P[v1|v2]+)/dashboard/deployChart$', DeployChart.as_view(), name='deploy_chart'), 47 | 48 | 49 | ] 50 | -------------------------------------------------------------------------------- /apps/k8s/views/logs.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : logs.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/2/4 14 | # @Desc : 15 | """ 16 | 17 | import traceback 18 | import logging 19 | 20 | from kubernetes.client import ApiException 21 | from rest_framework.response import Response 22 | from rest_framework.views import APIView 23 | from kubernetes import client 24 | 25 | from apps.account.models import User 26 | from utils.authorization import MyAuthentication 27 | from utils.http_response import APIResponse 28 | from utils.permissions import MyPermission 29 | 30 | logger = logging.getLogger('default') 31 | 32 | 33 | class LogsView(APIView): 34 | 35 | authentication_classes = [MyAuthentication] 36 | permission_classes = [MyPermission, ] 37 | 38 | def post(self, request, *args, **kwargs): 39 | """ 40 | 获取pod日志信息 41 | """ 42 | rsp = request.data 43 | pod = rsp['pod'] 44 | namespace = rsp['namespace'] 45 | tail_line = rsp.get('tail_line', 100) 46 | user_obj = User.objects.filter(username=request.user.username).first() 47 | user_group = user_obj.roles.all() 48 | 49 | if not request.user.is_superuser: 50 | 51 | if user_group: 52 | group = str(user_group[0]).strip() 53 | if group == 'develop' and namespace != 'develop' or namespace != 'dingtalk': 54 | return APIResponse(errcode=403, errmsg='无权限查看日志', data={'data': '无权限查看日志,请联系运维处理!'}) 55 | elif group == 'test' and namespace != 'release' or namespace != 'dingtalk': 56 | return APIResponse(errcode=403, errmsg='无权限查看日志', data={'data': '无权限查看日志,请联系运维处理!'}) 57 | else: 58 | return APIResponse(errcode=403, errmsg='无权限查看日志', data={'data': '无权限查看日志,请联系运维处理!'}) 59 | else: 60 | return APIResponse(errcode=403, errmsg='无权限查看日志', data={'data': '无权限查看日志,请联系运维处理!'}) 61 | 62 | logger.info('用户:%s, 请求查看pod日志: %s, 命名空间:%s' % (str(request.user.username), pod, namespace)) 63 | 64 | v1 = client.CoreV1Api() 65 | try: 66 | data = v1.read_namespaced_pod_log( 67 | name=pod, namespace=namespace, tail_lines=tail_line, follow=False, pretty=True 68 | ) 69 | 70 | return APIResponse(data=data.split('\n')) 71 | except ApiException as e: 72 | logger.warning("获取pod日志出错, 详细信息:%s" % str(traceback.format_exc())) 73 | return APIResponse(errcode=9999, errmsg='获取日志异常,请联系运维处理!', data={'data': '获取日志异常,请联系运维处理!'}) 74 | -------------------------------------------------------------------------------- /utils/permissions.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : permissions.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/1 14 | # @Desc : 15 | """ 16 | 17 | import re 18 | import logging 19 | 20 | from rest_framework.permissions import BasePermission 21 | from rest_framework.exceptions import PermissionDenied 22 | from django.urls import URLPattern 23 | 24 | from utils.jwt_token import parse_payload 25 | 26 | logger = logging.getLogger('default') 27 | 28 | 29 | class MyPermission(BasePermission): 30 | """ 31 | 权限认证 32 | """ 33 | 34 | def __init__(self): 35 | # 不需要权限的路径 36 | self.common_paths = ['/user/login/'] 37 | 38 | def has_permission(self, request, view): 39 | current_url = request.path_info 40 | method = request.method 41 | p = re.compile(r'([a-zA-Z]|[0-9]|[.])|(/.*)') 42 | url = p.findall(current_url)[0][1] 43 | if url == '/': 44 | raise PermissionDenied('不能访问该路径') 45 | for i in self.common_paths: 46 | i = '^{}$'.format(i) 47 | flag = re.match(i, url) 48 | if flag: 49 | return True 50 | try: 51 | token = request.auth 52 | if token is None: 53 | token = request.META.get('HTTP_AUTHORIZATION', '').split()[1] 54 | result = parse_payload(token) 55 | paths = [] 56 | permissions = result['data'].get('permissions', {}) 57 | for key in permissions.keys(): 58 | paths.append(key) 59 | except Exception: 60 | import traceback 61 | logger.error('权限认证失败: %s' % str(traceback.format_exc())) 62 | raise PermissionDenied('权限认证失败') 63 | 64 | for path in paths: 65 | tmp = path 66 | path = '^{}$'.format(path) 67 | flag = re.match(path, url) 68 | if flag: 69 | if method in permissions[tmp] or '*' in permissions[tmp]: 70 | return True 71 | logger.warning('用户无权限访问url:%s' % url) 72 | return False 73 | 74 | 75 | def get_all_paths(patterns, pre_fix, result): 76 | """ 77 | 获取项目URL 78 | """ 79 | for item in patterns: 80 | part = item.pattern.regex.pattern.strip("^$") 81 | if isinstance(item, URLPattern): 82 | if not pre_fix.startswith('/admin'): 83 | result.append(pre_fix.lstrip(r'[a-zA-Z]|v[a-zA-Z]|[0-9]|[.]|(/.*)') + part) 84 | else: 85 | get_all_paths(item.url_patterns, pre_fix + part, result=result) 86 | 87 | return result 88 | -------------------------------------------------------------------------------- /apps/k8s/views/namespace.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : namespace.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2020/12/7 14 | # @Desc : 15 | """ 16 | 17 | import traceback 18 | import logging 19 | 20 | from rest_framework.views import APIView 21 | from rest_framework.response import Response 22 | from kubernetes import client, config 23 | 24 | from utils.authorization import MyAuthentication 25 | from utils.permissions import MyPermission 26 | 27 | logger = logging.getLogger('default') 28 | config.load_kube_config() 29 | v1 = client.CoreV1Api() 30 | 31 | 32 | class NamespaceView(APIView): 33 | 34 | authentication_classes = [MyAuthentication] 35 | permission_classes = [MyPermission, ] 36 | 37 | """ 38 | 命名空间 39 | """ 40 | 41 | def get(self, request, *args, **kwargs): 42 | context = {'errcode': 0, 'msg': '获取成功', 'data': ''} 43 | try: 44 | ret = v1.list_namespace() 45 | tmp_context = [] 46 | for i in ret.items: 47 | tmp_dict = dict() 48 | tmp_dict['name'] = i.metadata.name 49 | tmp_dict['status'] = i.status.phase 50 | tmp_context.append(tmp_dict) 51 | context['data'] = tmp_context 52 | except Exception as e: 53 | logger.error('获取namespace失败:%s' % str(traceback.format_exc()), e) 54 | context['msg'] = '获取失败' 55 | context['errcode'] = 1000 56 | 57 | return Response(context) 58 | 59 | def post(self, request, *args, **kwargs): 60 | namespace = request.data.get('namespace') 61 | context = {'errcode': 0, 'msg': '创建成功'} 62 | try: 63 | body = client.V1Namespace(api_version='v1', kind='Namespace', metadata={'name': namespace}) 64 | ret = v1.create_namespace(body=body) 65 | if ret.status.phase == 'Active': 66 | return Response(context) 67 | except Exception as e: 68 | logger.error('创建namespace失败:%s' % str(traceback.format_exc()), e) 69 | context['errcode'] = 1000 70 | context['msg'] = '创建失败' 71 | return Response(context) 72 | 73 | def delete(self, request, *args, **kwargs): 74 | query_params = request.query_params.dict() 75 | namespace = query_params.get('namespace') 76 | context = {'errcode': 0, 'msg': '删除成功'} 77 | try: 78 | v1.delete_namespace(name=namespace) 79 | except Exception as e: 80 | logger.error('删除namespace失败:%s' % str(traceback.format_exc()), e) 81 | context['errcode'] = 1000 82 | context['msg'] = '删除失败' 83 | return Response(context) 84 | -------------------------------------------------------------------------------- /apps/cmdb/views/server.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/13 0013 下午 5:14 11 | @Author: micheng. 12 | @File: server.py 13 | """ 14 | 15 | from django.db.models import Q 16 | from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView 17 | from rest_framework.views import APIView 18 | 19 | from apps.cmdb.models import Server 20 | from apps.cmdb.serializers.server import ServerSerializer 21 | from utils.authorization import MyAuthentication 22 | from utils.http_response import APIResponse 23 | from utils.permissions import MyPermission 24 | from utils.pagination import MyPageNumberPagination 25 | 26 | 27 | class ServerView(ListAPIView): 28 | """ 29 | 资产列表 30 | """ 31 | 32 | authentication_classes = [MyAuthentication, ] 33 | permission_classes = [MyPermission, ] 34 | 35 | serializer_class = ServerSerializer 36 | pagination_class = MyPageNumberPagination 37 | 38 | def get_queryset(self, *args, **kwargs): 39 | 40 | search = self.request.query_params.get('keyword') 41 | 42 | qs = Server.objects.all() 43 | 44 | if search is not None: 45 | qs = qs.filter( 46 | Q(private_ip__icontains=search) | Q(public_ip__icontains=search) | Q(hostname__icontains=search) | Q( 47 | instance_id__icontains=search)) 48 | 49 | """Filter ECS Server.""" 50 | return qs 51 | 52 | 53 | class ServerDetailView(RetrieveUpdateDestroyAPIView): 54 | """ 55 | 资产详情 56 | """ 57 | 58 | authentication_classes = [MyAuthentication, ] 59 | permission_classes = [MyPermission, ] 60 | 61 | queryset = Server.objects.all() 62 | serializer_class = ServerSerializer 63 | 64 | 65 | class ServerSearchView(APIView): 66 | 67 | authentication_classes = [MyAuthentication, ] 68 | permission_classes = [MyPermission, ] 69 | 70 | def get(self, request, *args, **kwargs): 71 | """ 72 | 服务器搜索 73 | """ 74 | data = request.query_params 75 | search = data.get('keyword') 76 | query_set = Server.objects.all() 77 | 78 | if search is not None: 79 | query_set = Server.objects.filter( 80 | Q(private_ip__icontains=search) | Q(public_ip__icontains=search) | Q(hostname__icontains=search) | Q( 81 | instance_id__icontains=search)) 82 | 83 | paginator = MyPageNumberPagination() 84 | page_publish_list = paginator.paginate_queryset(query_set, self.request, view=self) 85 | ps = ServerSerializer(page_publish_list, many=True) 86 | response = paginator.get_paginated_response(ps.data) 87 | return APIResponse(data=response.data) 88 | 89 | 90 | -------------------------------------------------------------------------------- /devops_api/celery.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/14 0014 下午 12:11 11 | @Author: micheng. 12 | @File: celery.py 13 | """ 14 | 15 | from __future__ import absolute_import, unicode_literals 16 | import os 17 | from datetime import timedelta 18 | 19 | from decouple import config 20 | from django.conf import settings 21 | from celery import Celery, platforms 22 | from celery.schedules import crontab 23 | 24 | 25 | # set the default Django settings module for the 'celery' program. 26 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devops_api.settings') 27 | CELERY_BROKER_REDIS_PASS = config('CELERY_BROKER_REDIS_PASS') 28 | CELERY_BROKER_REDIS_HOST = config('CELERY_BROKER_REDIS_HOST') 29 | CELERY_BROKER_REDIS_PORT = config('CELERY_BROKER_REDIS_PORT') 30 | CELERY_BROKER_REDIS_DB = config('CELERY_BROKER_REDIS_DB') 31 | 32 | CELERY_BACKEND_REDIS_PASS = config('CELERY_BACKEND_REDIS_PASS') 33 | CELERY_BACKEND_REDIS_HOST = config('CELERY_BACKEND_REDIS_HOST') 34 | CELERY_BACKEND_REDIS_PORT = config('CELERY_BACKEND_REDIS_PORT') 35 | CELERY_BACKEND_REDIS_DB = config('CELERY_BACKEND_REDIS_DB') 36 | 37 | app = Celery('devops_api', 38 | broker=f'redis://:{CELERY_BROKER_REDIS_PASS}@{CELERY_BROKER_REDIS_HOST}:{CELERY_BROKER_REDIS_PORT}/{CELERY_BROKER_REDIS_DB}', 39 | backend=f'redis://:{CELERY_BACKEND_REDIS_PASS}@{CELERY_BACKEND_REDIS_HOST}:{CELERY_BACKEND_REDIS_PORT}/{CELERY_BACKEND_REDIS_DB}', 40 | include=['tasks.aliyun', 41 | 'tasks.domain', 42 | 'tasks.ansible_cmd', 43 | 'tasks.email', 44 | ]) 45 | 46 | # Using a string here means the worker doesn't have to serialize 47 | # the configuration object to child processes. 48 | # - namespace='CELERY' means all celery-related configuration keys 49 | # should have a `CELERY_` prefix. 50 | app.config_from_object('django.conf:settings', namespace='CELERY') 51 | 52 | # Load task modules from all registered Django app configs. 53 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 54 | 55 | # app.conf['imports'] = ['tasks.aliyun'] 56 | # 允许root 用户运行celery 57 | platforms.C_FORCE_ROOT = True 58 | 59 | app.conf.beat_schedule = { 60 | # Executes every Monday morning at 7:30 a.m. 61 | # https://docs.celeryproject.org/en/4.4.2/userguide/periodic-tasks.html 62 | 'sync_ecs': { 63 | 'task': 'tasks.aliyun.sync_ecs', 64 | 'schedule': crontab(minute=0, hour='*/4'), 65 | # 'args': ("1"), 66 | }, 67 | 'sync_cloud_disk': { 68 | 'task': 'tasks.aliyun.sync_cloud_disk', 69 | 'schedule': crontab(minute=0, hour='*/8'), 70 | # 'args': ("1"), 71 | }, 72 | # 'sync_domain': { 73 | # 'task': 'tasks.domain.sync_domain', 74 | # 'schedule': timedelta(seconds=1800), 75 | # } 76 | } 77 | -------------------------------------------------------------------------------- /apps/rbac/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-31 14:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Menu', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=32, unique=True, verbose_name='菜单名称')), 20 | ('icon', models.CharField(blank=True, max_length=32, verbose_name='图标')), 21 | ('path', models.CharField(blank=True, help_text='如果有子菜单,不需要填写该字段', max_length=100, verbose_name='链接地址')), 22 | ('is_active', models.BooleanField(default=True, verbose_name='激活状态')), 23 | ('sort', models.IntegerField(blank=True, verbose_name='排序标记')), 24 | ('pid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.menu', verbose_name='上级菜单')), 25 | ], 26 | options={ 27 | 'verbose_name': '菜单', 28 | 'verbose_name_plural': '菜单', 29 | 'ordering': ['sort'], 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='Permission', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('name', models.CharField(max_length=32, unique=True, verbose_name='权限名称')), 37 | ('path', models.CharField(blank=True, max_length=128, verbose_name='含正则的URL')), 38 | ('method', models.CharField(default='GET', max_length=16, verbose_name='方法')), 39 | ('pid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='rbac.permission', verbose_name='上级权限')), 40 | ], 41 | options={ 42 | 'verbose_name': '权限', 43 | 'verbose_name_plural': '权限', 44 | 'ordering': ['id'], 45 | }, 46 | ), 47 | migrations.CreateModel( 48 | name='Role', 49 | fields=[ 50 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 51 | ('name', models.CharField(max_length=32, unique=True, verbose_name='角色名称')), 52 | ('desc', models.CharField(blank=True, max_length=50, verbose_name='描述')), 53 | ('menus', models.ManyToManyField(blank=True, to='rbac.Menu', verbose_name='菜单')), 54 | ('permissions', models.ManyToManyField(blank=True, to='rbac.Permission', verbose_name='权限')), 55 | ], 56 | options={ 57 | 'verbose_name': '角色', 58 | 'verbose_name_plural': '角色', 59 | 'ordering': ['id'], 60 | }, 61 | ), 62 | ] 63 | -------------------------------------------------------------------------------- /apps/application/utils/check_heartbeat.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/27 0027 下午 12:18 11 | @Author: micheng. 12 | @File: check_heartbeat.py 13 | """ 14 | 15 | import logging 16 | import os 17 | import time 18 | 19 | import paramiko 20 | import requests 21 | from paramiko.ssh_exception import NoValidConnectionsError, AuthenticationException 22 | 23 | from devops_api.settings import BASE_DIR 24 | 25 | logger = logging.getLogger('default') 26 | 27 | 28 | class SendHeartBeat(object): 29 | 30 | @staticmethod 31 | def check_agent(ip: str) -> bool: 32 | """ 33 | :params ip 应用主机IP地址 34 | """ 35 | count = 0 36 | while True: 37 | count += 1 38 | try: 39 | check = requests.get(url='http://{}:8563/api'.format(ip), timeout=5) 40 | time.sleep(1) 41 | if check.status_code == 200: 42 | logger.info(f'agent存活状态检查, host: {ip} up: Running count: {count}') 43 | if count == 3: 44 | return True 45 | 46 | except BaseException as e: 47 | print(e) 48 | logger.error(f'agent客户端已离线, host: {ip}, up: Shutdown') 49 | return False 50 | 51 | @staticmethod 52 | def install_agent(name: str, ip: str) -> bool: 53 | """ 54 | :params name 应用名称 55 | :params ip 应用所在主机ip 56 | """ 57 | 58 | # todo ssh安装agent, 安装完成后,将session_id, consumer_id写入redis, 同时数据激活字段设置为1 59 | client = paramiko.SSHClient() 60 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 61 | # ssh 证书路径 62 | private_key = paramiko.RSAKey.from_private_key_file(os.path.join(BASE_DIR, 'utils', 'sshKey')) 63 | try: 64 | client.connect(hostname=ip, 65 | port=20199, 66 | username='ecs-user', 67 | pkey=private_key 68 | ) 69 | cmd = """java -jar /opt/arthas-boot.jar 70 | --attach-only 71 | --target-ip '%s' 72 | --agent-id '%s' `ps aux|grep -v grep |grep math |awk '{print $2}'` 73 | """ % (name, ip) 74 | stdin, stdout, stderr = client.exec_command(cmd) 75 | 76 | except NoValidConnectionsError as e: 77 | print('远程主机建立连接失败', e) 78 | logger.error(f'远程主机建立连接失败, 主机地址: {ip}') 79 | return False 80 | 81 | except AuthenticationException as e: 82 | print("密码错误", e) 83 | logger.error('由于密码错误, 远程主机建立连接失败!') 84 | return False 85 | 86 | else: 87 | if stderr.readlines(): 88 | logger.error('agent安装失败: %s' % stderr.read().decode('utf-8')) 89 | return False 90 | 91 | logger.info(f'{name} agent安装成功...') 92 | return True 93 | 94 | finally: 95 | client.close() 96 | -------------------------------------------------------------------------------- /apps/k8s/views/scale.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : scale.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/31 14 | # @Desc : 15 | """ 16 | 17 | import logging 18 | import time 19 | 20 | from rest_framework.views import APIView 21 | from kubernetes import client, config 22 | 23 | from utils.authorization import MyAuthentication 24 | from utils.http_response import APIResponse 25 | from utils.permissions import MyPermission 26 | 27 | logger = logging.getLogger('default') 28 | config.load_kube_config() 29 | apps_v1 = client.AppsV1Api() 30 | 31 | 32 | class DeploymentScaleViews(APIView): 33 | authentication_classes = [MyAuthentication] 34 | permission_classes = [MyPermission, ] 35 | 36 | def post(self, request, *args, **kwargs): 37 | """ 38 | deployment 扩缩容 39 | :param request: 40 | :return: 41 | """ 42 | if request.user.is_superuser: 43 | data = request.data 44 | deployment_number = int(data.get('scaleNumber', 1)) 45 | body = { 46 | "api_version": "v1", 47 | "kind": "deployment", 48 | "spec": {"replicas": deployment_number} 49 | } 50 | 51 | def deployment_status(): 52 | is_deployment_version = apps_v1.patch_namespaced_deployment_status(name=data.get('name'), 53 | namespace=data.get('namespace'), 54 | body=body, 55 | async_req=True) 56 | is_deployment_status = is_deployment_version.get() 57 | is_deployment_status = is_deployment_status.__dict__ 58 | is_deployment_version = is_deployment_status['_metadata'].__dict__ 59 | 60 | return is_deployment_version['_generation'] 61 | 62 | old_version = deployment_status() 63 | try: 64 | ret = apps_v1.patch_namespaced_deployment_scale(name=data.get('name'), namespace=data.get('namespace'), body=body) 65 | time.sleep(1) 66 | 67 | # body= async_req=True 异步任务 print(ret.get()) 取任务结果 68 | except Exception as e: 69 | print(e.args) 70 | logger.error('Pod缩放异常%s' % str(e.args)) 71 | return APIResponse(errcode=500, errmsg='Pod缩放异常!') 72 | time.sleep(1) 73 | new_version = deployment_status() 74 | 75 | if new_version > old_version: 76 | logger.info('Pod缩放成功:%s, version:%s' % (str(ret), str(old_version))) 77 | return APIResponse(data={'msg': 'Pod缩放成功'}) 78 | else: 79 | return APIResponse(data={"msg": "当前版本无变化!"}) 80 | logger.warning('%s用户无权限操作' % str(request.user.username)) 81 | return APIResponse(errcode=401, errmsg='你当前无权限操作!') 82 | -------------------------------------------------------------------------------- /apps/account/admin.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | import logging 9 | 10 | from openpyxl import Workbook 11 | from django.contrib import admin 12 | from django.contrib.auth.admin import UserAdmin 13 | from django.http import HttpResponse 14 | from django.utils.translation import gettext_lazy 15 | 16 | from apps.account.models import User 17 | 18 | logger = logging.getLogger('default') 19 | 20 | 21 | class ExportExcelMixin(object): 22 | """ 23 | 导出Excel 24 | """ 25 | def export_as_excel(self, request, queryset): 26 | meta = self.model._meta 27 | field_names = [field.name for field in meta.fields] 28 | 29 | response = HttpResponse(content_type='application/msexcel') 30 | response['Content-Disposition'] = f'attachment; filename={meta}.xlsx' 31 | wb = Workbook() 32 | ws = wb.active 33 | ws.append(field_names) 34 | for obj in queryset: 35 | for field in field_names: 36 | data = [f'{getattr(obj, field)}' for field in field_names] 37 | row = ws.append(data) 38 | 39 | wb.save(response) 40 | return response 41 | 42 | export_as_excel.short_description = '导出Excel' 43 | 44 | 45 | @admin.register(User) 46 | class UserProfileAdmin(UserAdmin, ExportExcelMixin): 47 | """继承并重写UserAdmin类, add_fieldsets添加字段在(django admin后台显示) 48 | 重写原因: 解决后台添加用户, 密码显示明文 49 | """ 50 | # 设置显示数据库中哪些字段 51 | list_display = ['username', 'name', 'email', 'mobile', 'position', 'hire_date', 'sex', 52 | 'is_active', 'is_staff', 'is_superuser', 'last_login', 'user_id', 'job_number', 'department'] 53 | # filter_horizontal = ['department', 'groups'] 54 | list_filter = ['position', 'is_active', 'is_staff', 'is_superuser'] 55 | list_display_links = ['name', 'email'] 56 | search_fields = ['username', 'name', 'mobile', 'email'] 57 | actions = ['export_as_excel'] 58 | filter_horizontal = ['groups'] 59 | fieldsets = ( 60 | (None, {'fields': ('username', 'password', 'name', 'email')}), 61 | (gettext_lazy('User Information'), {'fields': ('mobile', 'position', 'sex', 'hire_date', 'user_id', 'job_number', 'department')}), 62 | (gettext_lazy('Permissions'), {'fields': ('is_superuser', 'is_staff', 'is_active','groups', 'user_permissions')}), 63 | (gettext_lazy('Important dates'), {'fields': ('last_login', 'date_joined', 'roles')}), 64 | 65 | ) 66 | add_fieldsets = ( 67 | (None, { 68 | 'classes': ('wide',), 69 | 'fields': ( 70 | 'username', 'password1', 'password2', 'first_name', 'email', 71 | ), 72 | }), 73 | ('User Information', { 74 | 'fields': ( 75 | 'mobile', 'position', 'sex', 'hire_date', 'user_id', 'job_number', 'department', 'name' 76 | ), 77 | 78 | }), 79 | ('Permissions', { 80 | 'fields': ( 81 | 'is_superuser', 'is_staff', 'is_active', 'groups', 'user_permissions' 82 | ) 83 | }), 84 | ) 85 | list_per_page = 20 86 | -------------------------------------------------------------------------------- /apps/rbac/views/permission.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2020/12/1 0026 下午 13:03 11 | @Author: micheng. 12 | @File: permission.py 13 | """ 14 | 15 | from django.apps import apps 16 | from rest_framework import status 17 | from rest_framework.response import Response 18 | from rest_framework.views import APIView 19 | from rest_framework.viewsets import ModelViewSet 20 | 21 | from devops_api import urls 22 | from apps.rbac.models import Permission 23 | from apps.rbac.serializers import permission_serializers 24 | from utils.pagination import MaxPagination 25 | from utils.permissions import get_all_paths, MyPermission 26 | from utils.tree import tree_filter 27 | from utils.authorization import MyAuthentication 28 | 29 | 30 | class PermissionView(ModelViewSet): 31 | 32 | """ 33 | 权限管理 34 | """ 35 | pagination_class = MaxPagination 36 | queryset = Permission.objects.all() 37 | serializer_class = permission_serializers.PermissionSerializer 38 | 39 | 40 | class PermissionAll(APIView): 41 | authentication_classes = [MyAuthentication] 42 | permission_classes = [MyPermission, ] 43 | 44 | """ 45 | 获取所有权限 46 | """ 47 | 48 | def get(self, request, *args, **kwargs): 49 | queryset = Permission.objects.all() 50 | serializer = permission_serializers.PermissionSerializer(queryset, many=True) 51 | results = serializer.data 52 | return Response(results) 53 | 54 | 55 | class PermissionTree(APIView): 56 | authentication_classes = [MyAuthentication] 57 | permission_classes = [MyPermission, ] 58 | 59 | """ 60 | 权限树 61 | """ 62 | 63 | def get(self, request, *args, **kwargs): 64 | queryset = Permission.objects.all() 65 | serializer = permission_serializers.PermissionSerializer(queryset, many=True) 66 | results = tree_filter(serializer.data) 67 | return Response(results) 68 | 69 | 70 | class InitPermission(APIView): 71 | authentication_classes = [MyAuthentication] 72 | permission_classes = [MyPermission, ] 73 | 74 | """ 75 | 初始化权限表 76 | """ 77 | 78 | def get(self, request, *args, **kwargs): 79 | methods = {'GET': '查看', 'POST': '添加', 'DELETE': '删除', 'PUT': '修改'} 80 | model = apps.get_models() 81 | try: 82 | for i in model: 83 | if i._meta.app_label in ['auth', 'contenttypes', 'sessions']: 84 | continue 85 | for method, desc in methods.items(): 86 | Permission.objects.update_or_create(method=method, name=str(desc) + str(i._meta.verbose_name)) 87 | except Exception as e: 88 | print(e) 89 | content = {'msg': '初始化失败!'} 90 | return Response(content, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 91 | return Response({'msg': '初始化成功!'}) 92 | 93 | 94 | class PermissionPath(APIView): 95 | authentication_classes = [MyAuthentication] 96 | permission_classes = [MyPermission, ] 97 | 98 | """ 99 | 获取权限path 100 | """ 101 | 102 | def get(self, request, *args, **kwargs): 103 | path = get_all_paths(urls.urlpatterns, pre_fix="/", result=[], ) 104 | result = list(map(lambda x: '/' + x, path)) 105 | return Response({'path': result}) 106 | -------------------------------------------------------------------------------- /apps/cmdb/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | 5 | # for k,v in old: 6 | # instance_id = v.get('instance_id') 7 | # new_data = new_dict['instance_id'] 8 | # 9 | # print(new_dict) 10 | 11 | 12 | # for update in update_server: 13 | # temp = [] 14 | # insert_instance = next((item for item in ecs_results_data if item['instance_id'] == update), None) 15 | # for key, value in insert_instance.items(): 16 | # old_value = getattr(server_all_dict[update], key) 17 | # if value == old_value: 18 | # # 没有发生变化, 什么都不做 19 | # continue 20 | # msg = "ecs的{}, 由{}变更成了{}".format(key, old_value, value) 21 | # temp.append(msg) 22 | # 23 | # setattr(server_all_dict[update], key, value) 24 | # # getattr(db_disk_dict[slot], key) # ---> 每一项:数据库中值, 每一项:新增值 ---> value 25 | # 26 | # print(key, value, getattr(server_all_dict[update], key)) 27 | # 28 | # if temp: 29 | # # 如果temp不为空,说明字段发生了变化 30 | # server_all_dict[update].save() 31 | # row = "[更新硬盘] 槽位:{}, 更新的内容:{}".format(update, ';'.join(temp)) 32 | # record_str_list.append(row) 33 | 34 | # Server.objects.update_or_create(defaults=ecs_instance, instance_id=ecs_instance.get('instance_id')) 35 | 36 | 37 | # server_queryset_all = Server.objects.values() 38 | # new_dict = {} 39 | # for new_ecs_value in ecs_results_data: 40 | # instance_id = new_ecs_value.get(new_ecs_value) 41 | # new_dict[new_ecs_value] = new_ecs_value 42 | # 43 | # for old_server in server_queryset_all: 44 | # print(type(old_server)) 45 | # for k, v in old_server: 46 | # instance_id = v.get(new_ecs_value) 47 | # new_data = new_dict[new_ecs_value] 48 | # print(new_dict, new_data) 49 | # for k, v in server_queryset_all: 50 | # instance_id = v.get(instance_id) 51 | # new_data = new_dict[instance_id] 52 | # if v != new_data: 53 | # print(new_dict) 54 | 55 | # for old_server in [entry for entry in server_queryset_all]: 56 | # update = update_server.items() - old_server.items() 57 | # if update: 58 | # print('资产发生变化: {}'.format(update_server)) 59 | 60 | # Server.objects.update_or_create(defaults=update_server, instance_id=update_server.get('instance_id')) 61 | 62 | #  update_server = server_new_results & old_server_all 63 | # for update in update_server: 64 | # temp = [] 65 | # insert_instance = next((item for item in ecs_results_data if item['instance_id'] == update), None) 66 | # for key, value in insert_instance.items(): 67 | # old_value = getattr(server_all_dict[update], key) 68 | # if value == old_value: 69 | # # 没有发生变化, 什么都不做 70 | # continue 71 | # msg = "ecs的{}, 由{}变更成了{}".format(key, old_value, value) 72 | # temp.append(msg) 73 | # 74 | # setattr(server_all_dict[update], key, value) 75 | # # getattr(db_disk_dict[slot], key) # ---> 每一项:数据库中值, 每一项:新增值 ---> value 76 | # 77 | # print(key, value, getattr(server_all_dict[update], key)) 78 | # 79 | # if temp: 80 | # # 如果temp不为空,说明字段发生了变化 81 | # server_all_dict[update].save() 82 | # row = "[更新硬盘] 槽位:{}, 更新的内容:{}".format(update, ';'.join(temp)) 83 | # record_str_list.append(row) 84 | 85 | # Server.objects.update_or_create(defaults=ecs_instance, instance_id=ecs_instance.get('instance_id')) 86 | 87 | import datetime 88 | 89 | 90 | origin_date_str= "2021-08-27T16:00Z" 91 | utc_date = datetime.datetime.strptime(origin_date_str, "%Y-%m-%dT%H:%MZ") 92 | local_date = utc_date + datetime.timedelta(hours=8) 93 | local_date_str = datetime.datetime.strftime(local_date ,'%Y-%m-%d %H:%M') 94 | print(local_date_str ) -------------------------------------------------------------------------------- /controller/ansible/test_copy.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/26 0007 下午 14:11 11 | @Author: micheng. 12 | @File: test_copy.py 13 | """ 14 | 15 | import sys 16 | sys.path.insert(0, "../..") 17 | 18 | from controller.ansible.runner import AdHocRunner, CommandRunner, Options, get_default_options 19 | from controller.ansible.inventory import BaseInventory 20 | 21 | 22 | class TestCmd(object): 23 | def __init__(self): 24 | # self.host_data = [ 25 | # { 26 | # "hostname": "testserver", 27 | # "ip": "192.168.1.35", 28 | # "port": 22, 29 | # "username": "root", 30 | # "password": "xxxxx.", 31 | # "become": { 32 | # "method": "su", # su , sudo 33 | # "user": "root", # 默认以root用户执行 34 | # # "pass": "123456", 35 | # }, 36 | # }, 37 | # { 38 | # "hostname": "Server222", 39 | # "ip": "192.168.1.36", 40 | # "port": 22, 41 | # "username": "root", 42 | # "password": "xxxxxx", 43 | # "become": { 44 | # "method": "su", # su , sudo 45 | # "user": "root", # 默认以root用户执行 46 | # # "pass": "123456", 47 | # }, 48 | # }, 49 | # ] 50 | self.host_data = [ 51 | {'hostname': 'i-bp10rydhf43t7r07v8wf', 'ip': 'xxxx', 'become': {'method': 'sudo', 'user': ''}, 52 | 'username': 'mone', 'password': 'xxx', 'port': 20100}, 53 | 54 | { 55 | "hostname": "testserver", 56 | "ip": "192.168.1.325", 57 | "port": 22, 58 | "username": "root", 59 | "password": "xxxx.", 60 | "become": { 61 | "method": "su", # su , sudo 62 | "user": "root", # 默认以root用户执行 63 | # "pass": "123456", 64 | }, 65 | }, 66 | ] 67 | 68 | print(self.host_data) 69 | self.inventory = BaseInventory(self.host_data) 70 | self.runner = AdHocRunner(self.inventory) 71 | 72 | def test_run(self): 73 | # 异步任务 需要传递 task_time 任务所执行的时间, anync = 前端的task_time 180 74 | # poll 0 75 | 76 | # 在yaml palybook 中执行异步任务 77 | """" 78 | tasks: 79 | - shell: sleep 10; hostname -i 80 | async: 10 # 异步 81 | poll: 0 82 | 83 | """ 84 | tasks = [ 85 | {"action": {"module": "copy", "args": {"src": "/data/my-code/pigs/ansible_upload_file_tmp/000c29cd2487.zip", 86 | "dst": "/tmp/24e0af08-bc49-11eb-b4e5-000c29cd2487.zip"},"name": "run_whoami"}} 87 | ] 88 | 89 | ret = self.runner.run(tasks, "all", 111111133333333333333334) 90 | print(ret) 91 | # print(ret.results_summary) 92 | # print(ret.results_raw) 93 | 94 | def test_execute(self): 95 | inventory = BaseInventory(self.host_data) 96 | self.runner = CommandRunner(inventory) 97 | res = self.runner.execute('ls -l /tmp', 'all') 98 | print(res.results_command) 99 | print(res.results_raw) 100 | 101 | 102 | if __name__ == "__main__": 103 | p = TestCmd() 104 | p.test_run() 105 | 106 | # p.test_execute() 107 | -------------------------------------------------------------------------------- /apps/rbac/views/role.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2020/12/1 0026 上午 11:29 11 | @Author: micheng. 12 | @File: role.py 13 | """ 14 | 15 | from rest_framework.viewsets import ModelViewSet 16 | 17 | from apps.rbac.models import Role 18 | from apps.rbac.serializers import roles_serializers 19 | from utils.http_response import APIResponse 20 | from utils.tree import tree_filter 21 | 22 | 23 | class RoleView(ModelViewSet): 24 | """ 25 | 角色管理 26 | """ 27 | queryset = Role.objects.all() 28 | serializer_class = roles_serializers.RoleListSerializer 29 | 30 | def create(self, request, *args, **kwargs): 31 | data = request.data 32 | role_obj = Role.objects.filter(name=data['name']).first() 33 | if role_obj is not None and role_obj.name == data['name']: 34 | return APIResponse(errcode=400, errmsg='角色已存在') 35 | serializer = self.get_serializer(data=data) 36 | serializer.is_valid(raise_exception=True) 37 | self.perform_create(serializer) 38 | return APIResponse(data={'msg': '角色新增成功'}) 39 | 40 | def update(self, request, *args, **kwargs): 41 | partial = kwargs.pop('partial', False) 42 | instance = self.get_object() 43 | serializer = self.get_serializer(instance, data=request.data, partial=partial) 44 | serializer.is_valid(raise_exception=True) 45 | self.perform_update(serializer) 46 | 47 | if getattr(instance, '_prefetched_objects_cache', None): 48 | instance._prefetched_objects_cache = {} 49 | 50 | return APIResponse(data={'msg': '角色修改成功'}) 51 | 52 | def destroy(self, request, *args, **kwargs): 53 | instance = self.get_object() 54 | self.perform_destroy(instance) 55 | return APIResponse(data={'msg': '角色删除成功'}) 56 | 57 | def get_serializer_class(self): 58 | if self.action == 'list' or self.action == 'retrieve': 59 | return roles_serializers.RoleListSerializer 60 | 61 | return roles_serializers.RoleModifySerializer 62 | 63 | def list(self, request, *args, **kwargs): 64 | """ 65 | 树型化权限和菜单 66 | """ 67 | queryset = self.filter_queryset(self.get_queryset()) 68 | 69 | page = self.paginate_queryset(queryset) 70 | if page is not None: 71 | serializer = self.get_serializer(page, many=True) 72 | for item in serializer.data: 73 | permissions = tree_filter(item['permissions']) 74 | menus = tree_filter(item['menus']) 75 | item['permissions'] = permissions 76 | item['menus'] = menus 77 | return self.get_paginated_response(serializer.data) 78 | 79 | serializer = self.get_serializer(queryset, many=True) 80 | for item in serializer.data: 81 | permissions = tree_filter(item['permissions']) 82 | menus = tree_filter(item['menus']) 83 | item['permissions'] = permissions 84 | item['menus'] = menus 85 | return APIResponse(data=serializer.data) 86 | 87 | def retrieve(self, request, *args, **kwargs): 88 | """ 89 | 树型化权限和菜单 90 | """ 91 | instance = self.get_object() 92 | serializer = self.get_serializer(instance) 93 | permissions = tree_filter(serializer.data['permissions']) 94 | menus = tree_filter(serializer.data['menus']) 95 | serializer.data['permissions'] = permissions 96 | serializer.data['menus'] = menus 97 | return APIResponse(data=serializer.data) 98 | 99 | -------------------------------------------------------------------------------- /doc/1_init.md: -------------------------------------------------------------------------------- 1 | ### 一、创建钉钉扫码登陆 2 | https://developers.dingtalk.com/document/app/scan-qr-code-to-log-on-to-third-party-websites 3 | 4 | https://developers.dingtalk.com/document/app/scan-qr-code-to-login-isvapp 5 | ### 二、创建钉钉自建应用 6 | https://developers.dingtalk.com/document/app/create-orgapp 7 | 8 | ### 三、执行数据迁移 9 | - python manage.py makemigrations 10 | - python manage.py migrate 11 | - python manage.py createsuperuser --username guji --email gujiwork@outlook.com 12 | - python manage.py runserver 13 | ### 四、修改apps/account/view/dingtalk.py 14 | ``` 15 | .env DING_LOGIN_EMAIL_SUFFIX = 'pigs.com' 16 | 将邮箱修改为您公司钉钉绑定的邮箱 17 | ``` 18 | ### 五、配置Api接口转发 19 | ```angular2html 20 | yum install -y openresty 21 | 22 | upstream api { 23 | server localhost:8000; 24 | } 25 | server { 26 | listen 80; 27 | server_name _; 28 | root /data/webapps/pigs-web; 29 | 30 | # Load configuration files for the default server block. 31 | location / { 32 | try_files $uri $uri/ /index.html; 33 | add_header Access-Control-Allow-Origin *; 34 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 35 | add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; 36 | add_header Cache-Control no-cache; 37 | add_header Cache-Control private; 38 | expires -1; 39 | } 40 | 41 | 42 | location /api { 43 | proxy_set_header Host $host; 44 | proxy_set_header X-Real-IP $remote_addr; 45 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 46 | proxy_pass http://api; 47 | add_header Access-Control-Allow-Origin *; 48 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 49 | add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; 50 | proxy_pass_header Set-Cookie; 51 | } 52 | 53 | location /ws { 54 | proxy_set_header Host $host; 55 | proxy_set_header X-Real-IP $remote_addr; 56 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 57 | proxy_pass http://api; 58 | proxy_http_version 1.1; 59 | proxy_set_header Upgrade $http_upgrade; 60 | proxy_set_header Connection $connection_upgrade; 61 | proxy_set_header X-Forwarded-Proto $scheme; 62 | } 63 | 64 | location /admin/ { 65 | proxy_set_header Host $host; 66 | proxy_set_header X-Real-IP $remote_addr; 67 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 68 | proxy_pass http://api/admin/; 69 | } 70 | 71 | location /static/ { 72 | proxy_redirect off; 73 | proxy_set_header Host $host; 74 | proxy_set_header X-Real-IP $remote_addr; 75 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 76 | proxy_pass http://api/static/; 77 | proxy_http_version 1.1; 78 | proxy_set_header Upgrade $http_upgrade; 79 | proxy_set_header Connection $connection_upgrade; 80 | 81 | } 82 | access_log logs/pigs-access.log main; 83 | error_log logs/pigs-err.log; 84 | } 85 | 86 | ``` 87 | ### 六、导入权限表 88 | ```angular2html 89 | 1、导入rbac_role 角色表 90 | 91 | 2、导入rbac_menu 菜单表 92 | 93 | 3、导入rbac_permission 权限表 94 | 95 | 4、登录django后台为角色绑定权限和菜单,最后再把用户绑定到角色 96 | ``` 97 | 98 | ##### 启动监控程序 99 | ```angular2html 100 | 修改metrics_proxy.yaml配置文件 101 | 102 | ./metrics_proxy proxy -c metrics_proxy.yaml # 启动程序 103 | 104 | 修改.env 文件 api_metrics_url=metrics_proxy port和 api_metrics_token=metrics_proxy token 105 | ``` 106 | 107 | #### Ansible 任务 108 | ```angular2html 109 | yum install -y sshpass 110 | ``` 111 | 112 | #### Ansible 分发, 文件大小控制 113 | ```angular2html 114 | 在Nginx http{}中加入 client_max_body_size 20m; 115 | ``` -------------------------------------------------------------------------------- /controller/ansible/test_cmd.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/18 0007 下午 4:03 11 | @Author: micheng. 12 | @File: test_cmd.py 13 | """ 14 | 15 | import sys 16 | 17 | import unittest 18 | import pymongo 19 | 20 | from controller.ansible.runner import AdHocRunner, CommandRunner, Options, get_default_options 21 | from controller.ansible.inventory import BaseInventory 22 | 23 | sys.path.insert(0, "../..") 24 | 25 | 26 | class TestCmd(object): 27 | def __init__(self): 28 | # self.host_data = [ 29 | # { 30 | # "hostname": "testserver", 31 | # "ip": "192.168.1.35", 32 | # "port": 22, 33 | # "username": "root", 34 | # "password": "xxxxxx.", 35 | # "become": { 36 | # "method": "su", # su , sudo 37 | # "user": "root", # 默认以root用户执行 38 | # # "pass": "123456", 39 | # }, 40 | # }, 41 | # { 42 | # "hostname": "Server222", 43 | # "ip": "192.168.1.36", 44 | # "port": 27123, 45 | # "username": "root", 46 | # "password": "xxxxxxx...", 47 | # "become": { 48 | # "method": "su", # su , sudo 49 | # "user": "root", # 默认以root用户执行 50 | # # "pass": "123456", 51 | # }, 52 | # }, 53 | # ] 54 | self.host_data = [ 55 | {'hostname': 'i-bp10rydhf43t7r07v8wf', 'ip': '10.17.145.141', 'become': {'method': 'sudo', 'user': ''}, 56 | 'username': 'mone', 'password': 'xxxxx~xxx', 'port': 22}, 57 | 58 | { 59 | "hostname": "testserver", 60 | "ip": "192.168.1.125", 61 | "port": 22, 62 | "username": "root", 63 | "password": "xxxxx.", 64 | "become": { 65 | "method": "su", # su , sudo 66 | "user": "root", # 默认以root用户执行 67 | # "pass": "123456", 68 | }, 69 | }, 70 | ] 71 | 72 | print(self.host_data) 73 | self.inventory = BaseInventory(self.host_data) 74 | # options = get_default_options() 75 | # options['remote_user'] = 'test' 76 | # options['become_user'] = 'test' 77 | self.runner = AdHocRunner(self.inventory) 78 | 79 | def test_run(self): 80 | # 异步任务 需要传递 task_time 任务所执行的时间, anync = 前端的task_time 180 81 | # poll 0 82 | 83 | # 在yaml palybook 中执行异步任务 84 | """" 85 | tasks: 86 | - shell: sleep 10; hostname -i 87 | async: 10 # 异步 88 | poll: 0 89 | 90 | """ 91 | tasks = [ 92 | # {"action": {"module": "shell", "args": "sleep 60 && pwd"}, "name": "run_cmd", "async": 5, 93 | # "poll": 0}, 94 | {"action": {"module": "shell", "args": "ls -l /tmp"}, "name": "run_whoami"}, 95 | ] 96 | 97 | ret = self.runner.run(tasks, "all", 111111133333333333333334) 98 | print(ret) 99 | # print(ret.results_summary) 100 | # print(ret.results_raw) 101 | 102 | def test_execute(self): 103 | inventory = BaseInventory(self.host_data) 104 | self.runner = CommandRunner(inventory) 105 | res = self.runner.execute('ls -l /tmp', 'all') 106 | print(res.results_command) 107 | print(res.results_raw) 108 | 109 | 110 | if __name__ == "__main__": 111 | p = TestCmd() 112 | p.test_run() 113 | 114 | # p.test_execute() 115 | -------------------------------------------------------------------------------- /tasks/ansible_cmd.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : ansible_cmd.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/16 14 | # @Desc : 15 | """ 16 | 17 | import logging 18 | import time 19 | import traceback 20 | 21 | from celery.result import AsyncResult 22 | 23 | from apps.cmdb.models import AnsibleExecHistory, AnsibleSyncFile 24 | from devops_api.celery import app 25 | from controller.ansible.inventory import BaseInventory 26 | from controller.ansible.runner import AdHocRunner, PlayBookRunner, Options 27 | 28 | logger = logging.getLogger('default') 29 | 30 | 31 | @app.task 32 | def command_shell(host_data, options, tasks, job_id): 33 | 34 | print('ansible异步任务开始啦!--------------------------------------------->') 35 | 36 | try: 37 | inventory = BaseInventory(host_data) 38 | runner = AdHocRunner(inventory, options) 39 | p = runner.run(tasks, "all", execution_id=str(job_id)) 40 | print(p.results_raw) 41 | except BaseException as e: 42 | print(traceback.format_exc()) 43 | print("异步任务执行出错: %s" % str(e)) 44 | print('ansible异步任务返回啦!--------------------------------------------->') 45 | 46 | 47 | @app.task 48 | def playbook_command(host_data, options, job_id): 49 | 50 | print('ansible playbook异步任务开始啦!--------------------{%s}------------------------->' % job_id) 51 | 52 | try: 53 | job_id = str(job_id) 54 | logger.info("任务id: %s" % job_id) 55 | Options.playbook_path = options['playbook_path'] 56 | Options.timeout = options['timeout'] 57 | Options.passwords = '' 58 | inventory = BaseInventory(host_data) 59 | runner = PlayBookRunner(inventory=inventory, options=Options) 60 | p = runner.run(job_id=job_id) 61 | # runner = AdHocRunner(inventory, options) 62 | # p = runner.run(tasks, "all", execution_id=str(job_id)) 63 | print(p) 64 | 65 | except BaseException as e: 66 | print(traceback.format_exc()) 67 | print("异步任务执行出错: %s" % str(e)) 68 | print('ansible playbook异步任务返回啦!--------------------------------------------->') 69 | 70 | 71 | @app.task 72 | def check_celery_status(task_id): 73 | while True: 74 | result = AsyncResult(str(task_id)).state 75 | h_obj = AnsibleExecHistory.objects.filter(job_id=task_id).first() 76 | if result == 'SUCCESS': 77 | h_obj.job_status = 'SUCCESS' 78 | h_obj.save() 79 | break 80 | elif result == 'FAILURE': 81 | h_obj.job_status = 'FAILURE' 82 | h_obj.save() 83 | break 84 | elif result == 'REVOKED': 85 | h_obj.job_status = 'FAILURE' 86 | h_obj.save() 87 | break 88 | elif result == 'STARTED': 89 | h_obj.job_status = 'STARTED' 90 | h_obj.save() 91 | elif result == 'RETRY': 92 | h_obj.job_status = 'RETRY' 93 | h_obj.save() 94 | else: 95 | print(result) 96 | time.sleep(3) 97 | 98 | 99 | @app.task 100 | def ansible_sendfile_celery_status(task_id): 101 | while True: 102 | result = AsyncResult(str(task_id)).state 103 | h_obj = AnsibleSyncFile.objects.filter(job_id=task_id).first() 104 | if result == 'SUCCESS': 105 | h_obj.job_status = 'SUCCESS' 106 | h_obj.save() 107 | break 108 | elif result == 'FAILURE': 109 | h_obj.job_status = 'FAILURE' 110 | h_obj.save() 111 | break 112 | elif result == 'REVOKED': 113 | h_obj.job_status = 'FAILURE' 114 | h_obj.save() 115 | break 116 | elif result == 'STARTED': 117 | h_obj.job_status = 'STARTED' 118 | h_obj.save() 119 | elif result == 'RETRY': 120 | h_obj.job_status = 'RETRY' 121 | h_obj.save() 122 | else: 123 | print(result) 124 | time.sleep(3) 125 | -------------------------------------------------------------------------------- /consumer/webssh.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : webssh.py 11 | # @Author: 往事随风 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/4/30 14 | # @Desc : 15 | """ 16 | 17 | import logging 18 | import time 19 | from threading import Thread 20 | from threading import Event 21 | 22 | from channels.exceptions import StopConsumer 23 | from channels.generic.websocket import WebsocketConsumer 24 | 25 | from apps.account.models import User 26 | from controller.kube_pod_exec import KubeApi 27 | 28 | logger = logging.getLogger('default') 29 | 30 | 31 | class K8SStreamThread(Thread): 32 | def __init__(self, websocket, container_stream): 33 | Thread.__init__(self) 34 | self.websocket = websocket 35 | self.stream = container_stream 36 | self._stop_event = Event() 37 | 38 | def stop(self): 39 | self._stop_event.set() 40 | 41 | def run(self): 42 | while self.stream.is_open(): 43 | time.sleep(0.1) 44 | if not self.stream.is_open(): 45 | self.websocket.close() 46 | try: 47 | if self.stream.peek_stdout(): 48 | stdout = self.stream.read_stdout() 49 | self.websocket.send(stdout) 50 | if self.stream.peek_stderr(): 51 | stderr = self.stream.read_stderr() 52 | self.websocket.send(stderr) 53 | except Exception as err: 54 | self.stream.write_stdin('exit\r') 55 | self.stream.close() 56 | self.websocket.close() 57 | 58 | else: 59 | # self.websocket.send('\n由于长时间没有操作,连接已断开!', close=True) 60 | # self.stream.write_stdin('exit\r') 61 | self.stream.close() 62 | self.stop() 63 | self.websocket.close() 64 | # print("websocket连接关闭了哦") 65 | # print("stream连接关闭了哦") 66 | # self.websocket.close() 67 | 68 | 69 | class SSHConsumer(WebsocketConsumer): 70 | 71 | def __init__(self, *args, **kwargs): 72 | super().__init__(*args, **kwargs) 73 | self.namespace = '' 74 | self.name = '' 75 | self.cols = '' 76 | self.rows = '' 77 | self.stream = None 78 | self.kube_stream = None 79 | 80 | def connect(self): 81 | logger.info(self.scope["url_route"]) 82 | self.namespace = self.scope["url_route"]["kwargs"]["ns"] 83 | self.name = self.scope["url_route"]["kwargs"]["pod_name"] 84 | self.cols = self.scope["url_route"]["kwargs"]["cols"] 85 | self.rows = self.scope["url_route"]["kwargs"]["rows"] 86 | self.stream = KubeApi(self.name, self.namespace).pod_exec(self.cols, self.rows) 87 | # kube exec 88 | self.kub_stream = K8SStreamThread(self, self.stream) 89 | self.kub_stream.setDaemon(True) 90 | self.kub_stream.start() 91 | self.accept() 92 | 93 | name = self.scope['user'] 94 | user_obj = User.objects.filter(name=name).first() 95 | user_group = user_obj.roles.all() 96 | if user_group: 97 | group = str(user_group[0]).strip() 98 | if group == 'test' and self.namespace not in ['release', 'dingtalk']: 99 | self.check_permissions() 100 | elif group == 'develop' and self.namespace not in ['develop', 'dingtalk']: 101 | self.check_permissions() 102 | else: 103 | pass 104 | else: 105 | self.check_permissions() 106 | 107 | def disconnect(self, close_code): 108 | try: 109 | self.stream.write_stdin('exit\r') 110 | self.stream.close() 111 | self.close() 112 | finally: 113 | self.kub_stream.stop() 114 | raise StopConsumer() 115 | 116 | def receive(self, text_data=None, bytes_data=None): 117 | try: 118 | self.stream.write_stdin(text_data) 119 | except Exception: 120 | self.stream.write_stdin('exit\r') 121 | self.stream.close() 122 | self.kub_stream.stop() 123 | self.close() 124 | 125 | def check_permissions(self): 126 | self.send(text_data='您无权限访问, 请联系运维人员!\n') 127 | self.stream.close() 128 | self.kub_stream.stop() 129 | self.close() 130 | return False 131 | 132 | def websocket_disconnect(self, message): 133 | """ 134 | 客户端主动断开了连接 135 | :param message: 136 | :return: 137 | """ 138 | self.stream.close() 139 | self.kub_stream.stop() 140 | self.close() 141 | raise StopConsumer() 142 | 143 | -------------------------------------------------------------------------------- /tasks/aliyun.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/14 0014 下午 12:18 11 | @Author: micheng. 12 | @File: aliyun.py 13 | """ 14 | 15 | from celery import shared_task 16 | from django.db import transaction 17 | 18 | from controller.get_all_ecs import SyncECS 19 | from apps.cmdb.models import CloudAK, Server, Disk 20 | 21 | 22 | @shared_task 23 | def sync_ecs(): 24 | """定时更新ECS资产信息""" 25 | 26 | ak = CloudAK.objects.filter(active=True).values('access_key', 'access_secret') 27 | if not ak.exists(): 28 | print("ECS资产同步异常, 您可能未正确配置阿里云AK、或后台阿里云AK被未启用。") 29 | return False 30 | 31 | with transaction.atomic(): 32 | try: 33 | aliyun = SyncECS(ak[0]['access_key'], ak[0]['access_secret']) 34 | aliyun.get_region() 35 | ecs_results_data = aliyun.get_ecs() 36 | 37 | server_new_results = set() 38 | server_queryset_all = Server.objects.all() 39 | server_all_dict = {row.instance_id: row for row in server_queryset_all} 40 | old_server_all = set(server_all_dict) 41 | 42 | for instance in ecs_results_data: 43 | """ 将接口返回的数据取出instance_id字段存入空集合中""" 44 | server_new_results.add(instance.get('instance_id')) 45 | 46 | ecs_instance = server_new_results - old_server_all 47 | print('正在同步阿里云ECS实例信息, 此过程大约耗时10~30s...') 48 | for instance_id in ecs_instance: 49 | """ 新增服务器资产, 从ecs_results_data列表中获取指定的dict""" 50 | 51 | instance = next((item for item in ecs_results_data if item['instance_id'] == instance_id), None) 52 | Server.objects.create(**instance) 53 | print('本次插入数据库ECS实例={}'.format(instance)) 54 | 55 | # TODO: 资产更新, 遍历接口列表中的dict===数据库的数据。 如果字段发生了变化则更新 56 | # 删除资产 57 | remove_server = old_server_all - server_new_results 58 | Server.objects.filter(instance_id__in=remove_server, cloud_type='aliyun').delete() 59 | if remove_server: 60 | print("数据库中ECS实例数据与接口中不一致, 开始[删除ECS实例] instance:{}".format(','.join(remove_server))) 61 | 62 | except BaseException as e: 63 | print('发生了错误={}'.format(e)) 64 | raise sync_ecs.retry(exc=e, countdown=300, max_retries=3) 65 | 66 | 67 | @shared_task 68 | def sync_cloud_disk(): 69 | print("开始同步云硬盘....") 70 | # 获取所有区域下的磁盘信息 71 | # print(ecs.get_disk()) 72 | ak = CloudAK.objects.filter(active=True).values('access_key', 'access_secret') 73 | if not ak.exists(): 74 | print("ECS资产同步异常, 您可能未正确配置阿里云AK、或后台阿里云AK被未启用。") 75 | return False 76 | 77 | with transaction.atomic(): 78 | try: 79 | aliyun = SyncECS(ak[0]['access_key'], ak[0]['access_secret']) 80 | aliyun.get_region() 81 | disk_results = aliyun.get_disk() 82 | 83 | disk_dict = {} 84 | for disk in disk_results: 85 | """ 将接口返回的数据取出instance_id字段存入空集合中""" 86 | if disk is not None: 87 | disk_dict['instance_id'] = disk['InstanceId'] 88 | disk_dict['disk_id'] = disk['DiskId'] 89 | disk_dict['disk_name'] = disk['DiskName'] 90 | disk_dict['category'] = disk['Category'] 91 | disk_dict['device'] = disk['Device'] 92 | disk_dict['enable_auto_snapshot'] = disk['EnableAutoSnapshot'] 93 | disk_dict['encrypted'] = disk['Encrypted'] 94 | disk_dict['create_time'] = disk['CreationTime'] 95 | disk_dict['attached_time'] = disk['AttachedTime'] 96 | disk_dict['disk_charge_type'] = disk['DiskChargeType'] 97 | disk_dict['delete_with_instance'] = disk['DeleteWithInstance'] 98 | disk_dict['expired_time'] = disk['ExpiredTime'] 99 | disk_dict['description'] = disk['Description'] 100 | disk_dict['size'] = disk['Size'] 101 | disk_dict['status'] = disk['Status'] 102 | disk_dict['tags'] = disk['Tags'] 103 | disk_dict['serial_number'] = disk['SerialNumber'] 104 | disk_dict['type'] = disk['Type'] 105 | disk_dict['portable'] = disk['Portable'] 106 | disk_dict['zone_id'] = disk['ZoneId'] 107 | 108 | Disk.objects.update_or_create(defaults=disk_dict, disk_id=disk_dict.get('disk_id')) 109 | 110 | print('已完成硬盘同步...') 111 | 112 | except Exception as e: 113 | raise sync_cloud_disk.retry(exc=e, countdown=300, max_retries=3) 114 | -------------------------------------------------------------------------------- /apps/account/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-05-31 14:55 2 | 3 | import datetime 4 | from django.conf import settings 5 | import django.contrib.auth.models 6 | import django.contrib.auth.validators 7 | from django.db import migrations, models 8 | import django.db.models.deletion 9 | import django.utils.timezone 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('rbac', '0001_initial'), 18 | ('auth', '0012_alter_user_first_name_max_length'), 19 | ] 20 | 21 | operations = [ 22 | migrations.CreateModel( 23 | name='User', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('password', models.CharField(max_length=128, verbose_name='password')), 27 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 28 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 29 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 30 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 31 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 32 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 33 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 34 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 35 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 36 | ('user_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='用户ID')), 37 | ('mobile', models.CharField(max_length=11, verbose_name='手机号')), 38 | ('name', models.CharField(max_length=32, verbose_name='姓名')), 39 | ('job_number', models.CharField(blank=True, max_length=32, null=True, verbose_name='工号')), 40 | ('position', models.CharField(blank=True, max_length=64, null=True, verbose_name='职位信息')), 41 | ('hire_date', models.DateTimeField(null=True, verbose_name='入职时间')), 42 | ('avatar', models.URLField(blank=True, null=True, verbose_name='用户头像')), 43 | ('sex', models.CharField(choices=[('man', '男'), ('women', '女')], default='man', max_length=8, verbose_name='性别')), 44 | ('department', models.CharField(blank=True, max_length=128, null=True, verbose_name='部门')), 45 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 46 | ('roles', models.ManyToManyField(blank=True, to='rbac.Role', verbose_name='角色')), 47 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 48 | ], 49 | options={ 50 | 'verbose_name': '用户信息', 51 | 'verbose_name_plural': '用户信息', 52 | 'db_table': 'users', 53 | 'ordering': ['-id'], 54 | }, 55 | managers=[ 56 | ('objects', django.contrib.auth.models.UserManager()), 57 | ], 58 | ), 59 | migrations.CreateModel( 60 | name='History', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 63 | ('ip', models.CharField(max_length=50)), 64 | ('type', models.CharField(default='email', max_length=20, verbose_name='登录类型:邮箱、钉钉')), 65 | ('created_at', models.CharField(default=datetime.datetime(2021, 5, 31, 14, 55, 42, 777850), max_length=20)), 66 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 67 | ], 68 | options={ 69 | 'db_table': 'login_history', 70 | 'ordering': ['-id'], 71 | }, 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /utils/time_utils.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/4/19 0019 上午 11:34 11 | @Author: micheng. 12 | @File: time_utils.py 13 | """ 14 | 15 | import pytz 16 | from datetime import date 17 | from datetime import datetime 18 | from datetime import timedelta 19 | 20 | loc_tz = pytz.timezone('Asia/Shanghai') 21 | utc_tz = pytz.timezone('UTC') 22 | 23 | 24 | def str2datetime_by_format(dt_str, dt_format='%Y-%m-%d %H:%M:%S'): 25 | """时间字符串转datetime""" 26 | 27 | return loc_tz.localize(datetime.strptime(dt_str, dt_format)) 28 | 29 | 30 | def datetime2str_by_format(dt, dt_format='%Y-%m-%d %H:%M:%S'): 31 | """本地datetime转本地字符串""" 32 | 33 | if not dt: 34 | return '' 35 | return dt.astimezone(loc_tz).strftime(dt_format) 36 | 37 | 38 | def date2str(dt, dt_format='%Y-%m-%d'): 39 | """日期转字符串""" 40 | 41 | if not dt: 42 | return '' 43 | return dt.strftime(dt_format) 44 | 45 | 46 | def str2date(dt_str): 47 | """ 48 | 字符串转日期 49 | """ 50 | dt = str2datetime_by_format(dt_str, '%Y-%m-%d') 51 | return dt.date() 52 | 53 | 54 | def date2datetime(dt): 55 | return today().replace(year=dt.year, month=dt.month, day=dt.day) 56 | 57 | 58 | def datetime2date_range(dt): 59 | """datetime转换成一天的开始和结束时间[start, end)""" 60 | 61 | start = dt.replace(hour=0, minute=0, second=0, microsecond=0) 62 | end = start + timedelta(days=1) 63 | return start, end 64 | 65 | 66 | def now(): 67 | return datetime.utcnow().replace(tzinfo=utc_tz).astimezone(loc_tz) 68 | 69 | 70 | def today(): 71 | dt = now() 72 | dt = dt.replace(hour=0, minute=0, second=0, microsecond=0) 73 | return dt 74 | 75 | 76 | def tomorrow(): 77 | return today() + timedelta(days=1) 78 | 79 | 80 | def yesterday(): 81 | return today() - timedelta(days=1) 82 | 83 | 84 | def utcts2dt(ts): 85 | """UTC时间戳转Datetime""" 86 | 87 | dt = datetime.utcfromtimestamp(ts) 88 | dt = dt + timedelta(hours=8) 89 | return dt 90 | 91 | 92 | def get_all_week_by_year(year): 93 | """获取一年所有周字符串列表""" 94 | 95 | week_list = [] 96 | for i in range(1, datetime.date(year, 12, 31).isocalendar()[1] + 1): 97 | week = '{}-{}'.format(year, i) 98 | week_list.append(week) 99 | return week_list 100 | 101 | 102 | def get_all_month_by_year(year): 103 | """获取一年所有月份字符串列表""" 104 | 105 | month_list = [] 106 | for i in range(12): 107 | month = '{}-{}'.format(year, i + 1) 108 | month_list.append(month) 109 | return month_list 110 | 111 | 112 | def get_all_month(dt_start, dt_end): 113 | """获取时间范围内所有月份""" 114 | 115 | month_list = [] 116 | dvalue = dt_end.year - dt_start.year 117 | if dvalue == 0: 118 | for i in range(dt_start.month, dt_end.month + 1): 119 | month = '{}-{}'.format(dt_start.year, i) 120 | month_list.append(month) 121 | elif dvalue == 1: 122 | for i in range(dt_start.month, 13): 123 | month = '{}-{}'.format(dt_start.year, i) 124 | month_list.append(month) 125 | for i in range(1, dt_end.month + 1): 126 | month = '{}-{}'.format(dt_end.year, i) 127 | month_list.append(month) 128 | elif dvalue > 1: 129 | for i in range(dt_start.month, 13): 130 | month = '{}-{}'.format(dt_start.year, i) 131 | month_list.append(month) 132 | for i in range(1, dvalue): 133 | month_list.extend(get_all_month_by_year(dt_start.year + i)) 134 | for i in range(1, dt_end.month + 1): 135 | month = '{}-{}'.format(dt_end.year, i) 136 | month_list.append(month) 137 | return month_list 138 | 139 | 140 | def get_max_week_by_year(year): 141 | """获取一年最大周数""" 142 | 143 | # 取一年中最后一天的周数,如果所在年已经不是同一年,那么再减去对应星期数 144 | week = date(year, 12, 31).isocalendar() 145 | if week[0] == year: 146 | return week[1] 147 | else: 148 | return date(year, 12, 31 - week[2]).isocalendar()[1] 149 | 150 | 151 | def get_all_week(dt_start, dt_end): 152 | """获取时间范围内所有周""" 153 | 154 | week_list = [] 155 | dvalue = dt_end.year - dt_start.year 156 | if dvalue == 0: 157 | for i in range(dt_start.isocalendar()[1], dt_end.isocalendar()[1] + 1): 158 | week = '{}-{}'.format(dt_start.year, i) 159 | week_list.append(week) 160 | elif dvalue == 1: 161 | max_week = get_max_week_by_year(dt_start.year) 162 | for i in range(dt_start.isocalendar()[1], max_week + 1): 163 | week = '{}-{}'.format(dt_start.year, i) 164 | week_list.append(week) 165 | for i in range(1, dt_end.isocalendar()[1] + 1): 166 | week = '{}-{}'.format(dt_end.year, i) 167 | week_list.append(week) 168 | elif dvalue > 1: 169 | for i in range(dt_start.isocalendar()[1], dt_start.replace(month=12, day=31).isocalendar()[1] + 1): 170 | week = '{}-{}'.format(dt_start.year, i) 171 | week_list.append(week) 172 | for i in range(1, dvalue): 173 | week_list.extend(get_all_week_by_year(dt_start.year + i)) 174 | for i in range(1, dt_end.isocalendar()[1] + 1): 175 | week = '{}-{}'.format(dt_end.year, i) 176 | week_list.append(week) 177 | return week_list 178 | 179 | 180 | def tz_to_localtime(tztime): 181 | """ 182 | 2022-01-09T16:00Z 转换为本地时间 183 | """ 184 | utc_date = datetime.strptime(tztime, "%Y-%m-%dT%H:%MZ") 185 | local_date = utc_date + timedelta(hours=8) 186 | local_date_str = datetime.strftime(local_date, '%Y-%m-%d %H:%M') 187 | print(local_date_str) 188 | 189 | return local_date_str -------------------------------------------------------------------------------- /controller/ansible/inventory.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/7 0007 下午 4:03 11 | @Author: micheng. 12 | @File: inventory.py 13 | """ 14 | 15 | from ansible.inventory.host import Host 16 | from ansible.vars.manager import VariableManager 17 | from ansible.inventory.manager import InventoryManager 18 | from ansible.parsing.dataloader import DataLoader 19 | 20 | 21 | __all__ = [ 22 | 'BaseHost', 'BaseInventory' 23 | ] 24 | 25 | 26 | class BaseHost(Host): 27 | def __init__(self, host_data): 28 | """ 29 | 初始化 30 | :param host_data: { 31 | "hostname": "", 32 | "ip": "", 33 | "port": "", 34 | # behind is not must be required 35 | "username": "", 36 | "password": "", 37 | "private_key": "", 38 | "become": { 39 | "method": "", 40 | "user": "", 41 | "pass": "", 42 | } 43 | "groups": [], 44 | "vars": {}, 45 | } 46 | """ 47 | self.host_data = host_data 48 | hostname = host_data.get('hostname') or host_data.get('ip') 49 | port = host_data.get('port') or 22 50 | super().__init__(hostname, port) 51 | self.__set_required_variables() 52 | self.__set_extra_variables() 53 | 54 | def __set_required_variables(self): 55 | host_data = self.host_data 56 | self.set_variable('ansible_host', host_data['ip']) 57 | self.set_variable('ansible_port', host_data['port']) 58 | 59 | if host_data.get('username'): 60 | self.set_variable('ansible_user', host_data['username']) 61 | 62 | # 添加密码和秘钥 63 | if host_data.get('password'): 64 | self.set_variable('ansible_ssh_pass', host_data['password']) 65 | if host_data.get('private_key'): 66 | self.set_variable('ansible_ssh_private_key_file', host_data['private_key']) 67 | 68 | # 添加become支持 69 | become = host_data.get("become", False) 70 | if become: 71 | self.set_variable("ansible_become", True) 72 | self.set_variable("ansible_become_method", become.get('method', 'sudo')) 73 | self.set_variable("ansible_become_user", become.get('user', 'root')) 74 | self.set_variable("ansible_become_pass", become.get('pass', '')) 75 | else: 76 | self.set_variable("ansible_become", False) 77 | 78 | def __set_extra_variables(self): 79 | for k, v in self.host_data.get('vars', {}).items(): 80 | self.set_variable(k, v) 81 | 82 | def __repr__(self): 83 | return self.name 84 | 85 | 86 | class BaseInventory(InventoryManager): 87 | """ 88 | 提供生成Ansible inventory对象的方法 89 | """ 90 | loader_class = DataLoader 91 | variable_manager_class = VariableManager 92 | host_manager_class = BaseHost 93 | 94 | def __init__(self, host_list=None, group_list=None): 95 | """ 96 | 用于生成动态构建Ansible Inventory. super().__init__ 会自动调用 97 | host_list: [{ 98 | "hostname": "", 99 | "ip": "", 100 | "port": "", 101 | "username": "", 102 | "password": "", 103 | "private_key": "", 104 | "become": { 105 | "method": "", 106 | "user": "", 107 | "pass": "", 108 | }, 109 | "groups": [], 110 | "vars": {}, 111 | }, 112 | ] 113 | group_list: [ 114 | {"name: "", children: [""]}, 115 | ] 116 | :param host_list: 117 | :param group_list 118 | """ 119 | self.host_list = host_list or [] 120 | self.group_list = group_list or [] 121 | assert isinstance(host_list, list) 122 | self.loader = self.loader_class() 123 | self.variable_manager = self.variable_manager_class() 124 | super().__init__(self.loader) 125 | 126 | def get_groups(self): 127 | return self._inventory.groups 128 | 129 | def get_group(self, name): 130 | return self._inventory.groups.get(name, None) 131 | 132 | def get_or_create_group(self, name): 133 | group = self.get_group(name) 134 | if not group: 135 | self.add_group(name) 136 | return self.get_or_create_group(name) 137 | else: 138 | return group 139 | 140 | def parse_groups(self): 141 | for g in self.group_list: 142 | parent = self.get_or_create_group(g.get("name")) 143 | children = [self.get_or_create_group(n) for n in g.get('children', [])] 144 | for child in children: 145 | parent.add_child_group(child) 146 | 147 | def parse_hosts(self): 148 | group_all = self.get_or_create_group('all') 149 | ungrouped = self.get_or_create_group('ungrouped') 150 | for host_data in self.host_list: 151 | host = self.host_manager_class(host_data=host_data) 152 | self.hosts[host_data['hostname']] = host 153 | groups_data = host_data.get('groups') 154 | if groups_data: 155 | for group_name in groups_data: 156 | group = self.get_or_create_group(group_name) 157 | group.add_host(host) 158 | else: 159 | ungrouped.add_host(host) 160 | group_all.add_host(host) 161 | 162 | def parse_sources(self, cache=False): 163 | self.parse_groups() 164 | self.parse_hosts() 165 | 166 | def get_matched_hosts(self, pattern): 167 | return self.get_hosts(pattern) -------------------------------------------------------------------------------- /consumer/ecs_webssh.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | # @File : ecs_webssh.py 11 | # @Author: 风哥 12 | # @Email: gujiwork@outlook.com 13 | # @Date : 2021/5/11 14 | # @Desc : 15 | """ 16 | 17 | import json 18 | from threading import Thread 19 | 20 | import paramiko 21 | from paramiko.client import SSHClient, AutoAddPolicy 22 | from paramiko.config import SSH_PORT 23 | from paramiko.rsakey import RSAKey 24 | from paramiko.ssh_exception import AuthenticationException 25 | from io import StringIO 26 | from channels.generic.websocket import WebsocketConsumer 27 | 28 | from apps.account.models import User 29 | from apps.cmdb.models import Server 30 | from apps.cmdb.models import EcsAuthSSH 31 | 32 | 33 | class SSH(object): 34 | def __init__(self, hostname, port=SSH_PORT, username='root', pkey=None, password=None, connect_timeout=10): 35 | if pkey is None and password is None: 36 | raise Exception('public key and password must have one is not None') 37 | self.client = None 38 | self.arguments = { 39 | 'hostname': hostname, 40 | 'port': port, 41 | 'username': username, 42 | 'password': password, 43 | 'pkey': RSAKey.from_private_key(StringIO(pkey)) if isinstance(pkey, str) else pkey, 44 | 'timeout': connect_timeout, 45 | } 46 | 47 | def get_client(self): 48 | if self.client is not None: 49 | return self.client 50 | self.client = SSHClient() 51 | self.client.set_missing_host_key_policy(AutoAddPolicy) 52 | self.client.connect(**self.arguments) 53 | return self.client 54 | 55 | def __enter__(self): 56 | if self.client is not None: 57 | raise RuntimeError('Already connected') 58 | return self.get_client() 59 | 60 | def __exit__(self, exc_type, exc_val, exc_tb): 61 | self.client.close() 62 | self.client = None 63 | 64 | 65 | class EcsSSHConsumer(WebsocketConsumer): 66 | def __init__(self, *args, **kwargs): 67 | super().__init__(*args, **kwargs) 68 | # self.user = self.scope['user'] 69 | # print(self.scope) 70 | self.id = None 71 | # self.id = self.scope['url_route']['kwargs']['ecs_id'] 72 | self.chan = None 73 | self.ssh = None 74 | self.width = None 75 | self.height = None 76 | 77 | def loop_read(self): 78 | while True: 79 | data = self.chan.recv(32 * 1024) 80 | print('read: {!r}'.format(data)) 81 | if not data: 82 | self.close(3333) 83 | break 84 | self.send(text_data=data.decode()) 85 | 86 | def receive(self, text_data=None, bytes_data=None): 87 | data = text_data or bytes_data 88 | if data: 89 | data = json.loads(data) 90 | print('write: {!r}'.format(data)) 91 | resize = data.get('resize') 92 | if resize and len(resize) == 2: 93 | self.chan.resize_pty(*resize) 94 | else: 95 | self.chan.send(data['data']) 96 | 97 | def disconnect(self, code): 98 | self.ssh.close() 99 | self.chan.close() 100 | print('Connection close') 101 | 102 | def connect(self): 103 | print('connect ing', self.scope) 104 | self.id = self.scope['url_route']['kwargs'].get('ecs_id') 105 | self.width = int(self.scope['url_route']['kwargs'].get('cols')) 106 | self.height = int(self.scope['url_route']['kwargs'].get('rows')) 107 | self.accept() 108 | self.send(text_data='Connecting ...\r\n') 109 | name = self.scope['user'] 110 | user_obj = User.objects.filter(name=name).first() 111 | user_group = user_obj.roles.all() 112 | print("用户: %s, 所属权限组:%s, 连接了ecs资产: %s" % (str(user_group[0]).strip(), str(user_group[0]).strip(), str(self.id))) 113 | if user_group: 114 | group = str(user_group[0]).strip() 115 | if group == 'ops' and user_obj.is_superuser: 116 | pass 117 | else: 118 | self.check_permissions() 119 | else: 120 | self.check_permissions() 121 | 122 | self._init() 123 | 124 | def _init(self): 125 | host = Server.objects.filter(pk=self.id).first() 126 | if not host: 127 | self.send(text_data='Unknown host\r\n') 128 | self.close() 129 | try: 130 | 131 | ssh_obj = EcsAuthSSH.objects.filter(server_type='linux').first() 132 | # raise Exception('私钥或者密码必须有一个不为空') 133 | print('连接的服务器id:', self.id) 134 | 135 | print(ssh_obj.type) 136 | if ssh_obj is not None: 137 | if ssh_obj.type == 'password' and self.id: 138 | ssh = SSH(hostname=host.private_ip, port=ssh_obj.port, username=ssh_obj.username, 139 | password=ssh_obj.password, ) 140 | elif ssh_obj.type == 'key' and self.id: 141 | ssh = SSH(hostname=host.private_ip, port=ssh_obj.port, username=ssh_obj.username, 142 | pkey=ssh_obj.key, ) 143 | else: 144 | self.close() 145 | return '没有配置认证方式' 146 | 147 | self.ssh = ssh.get_client() 148 | self.chan = self.ssh.invoke_shell(term='xterm') 149 | self.chan.transport.set_keepalive(30) 150 | Thread(target=self.loop_read).start() 151 | except Exception as e: 152 | print('连接出现异常', e) 153 | self.send(text_data=f'主机连接失败: {e}\r\n') 154 | self.close() 155 | return 156 | 157 | def check_permissions(self): 158 | self.send(text_data='无权限访问, 请联系运维人员!\n') 159 | self.close() 160 | return False 161 | -------------------------------------------------------------------------------- /apps/application/views/diagnosis.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/26 0026 上午 11:03 11 | @Author: micheng. 12 | @File: diagnosis.py 13 | """ 14 | 15 | import logging 16 | import json 17 | import time 18 | 19 | import demjson 20 | import requests 21 | from requests import ConnectTimeout 22 | 23 | from django.db import transaction 24 | from django_redis import get_redis_connection 25 | from rest_framework.generics import ListAPIView 26 | from rest_framework.views import APIView 27 | 28 | from apps.application.serializers.app_diagnosis import DiagnosisSerializer 29 | from apps.application.models import Diagnosis 30 | from apps.application.utils.check_heartbeat import SendHeartBeat 31 | from utils.authorization import MyAuthentication 32 | from utils.permissions import MyPermission 33 | from utils.http_response import APIResponse 34 | 35 | logger = logging.getLogger('default') 36 | 37 | 38 | class ArtHasInstallView(APIView): 39 | authentication_classes = [] 40 | permission_classes = [] 41 | 42 | def post(self, request, *args, **kwargs): 43 | """ 44 | 安装arthas 诊断客户端 45 | """ 46 | 47 | try: 48 | request_data = request.data 49 | query = Diagnosis.objects.filter(name=request_data['agentId']).values('name', 'ip', 'is_active') 50 | app_name = query[0]['name'] 51 | app_ip = query[0]['ip'] 52 | 53 | if SendHeartBeat.check_agent(app_ip): 54 | logger.info(f'name: {app_name} ip: {app_ip} 心跳正常, 请勿重复安装agent!') 55 | return APIResponse(errcode=0, errmsg='心跳正常, 请勿重复安装agent!') 56 | 57 | elif SendHeartBeat.install_agent(name=app_name, ip=app_ip): 58 | try: 59 | # todo 安装arthas客户端, 这里调用celery异步任务 60 | Diagnosis.objects.filter(name=request_data['agentId']).update(is_active='1') 61 | logger.info(f'诊断Agent安装成功 name: {app_name} ') 62 | return APIResponse(errcode=0, errmsg='诊断Agent安装成功') 63 | except BaseException as e: 64 | print(e) 65 | logger.error(f'{app_name} {app_ip} agent安装成功, 更新数据库失败!') 66 | else: 67 | return APIResponse(errcode=5000, errmsg='系统异常, 安装诊断客户端失败') 68 | 69 | except BaseException as e: 70 | print(e) 71 | logger.error(f'系统异常, 安装诊断客户端失败: {e}') 72 | return APIResponse(errcode=5001, errmsg='系统异常, 安装诊断客户端失败') 73 | 74 | 75 | class ArtHasView(APIView): 76 | 77 | authentication_classes = [MyAuthentication] 78 | permission_classes = [MyPermission, ] 79 | 80 | def post(self, request, *args, **kwargs): 81 | 82 | request_data = request.data 83 | try: 84 | query = Diagnosis.objects.filter(name=request_data['agentId']).values('name', 'ip', 'is_active') 85 | print(request_data, query) 86 | app_ip = query[0]['ip'] 87 | url = f"http://{app_ip}:8563/api" 88 | headers = { 89 | "Content-Type": "application/json;charset=utf-8" 90 | } 91 | send_data = { 92 | "action": "exec", 93 | "command": None, 94 | } 95 | init = {"action": "init_session"} 96 | rsp = requests.post(url=url, data=json.dumps(init), headers=headers, timeout=5) 97 | 98 | if rsp.status_code == 200: 99 | print('请求原始数据: %s' % request_data) 100 | logger.info(f'请求原始数据: {request_data}') 101 | if request_data['command'] == 'thread': 102 | # 查询所有线程 thread --all 103 | send_data['command'] = "thread --all" 104 | 105 | elif request_data['command'] == 'dashboard -n 1': 106 | # 获取dashboard 数据 dashboard -n 1 107 | send_data['command'] = "dashboard -n 1" 108 | 109 | elif request_data['type'] == 'queryThread': 110 | send_data['command'] = "%s" % request_data['command'] 111 | 112 | elif request_data['type'] == 'queryJVM': 113 | # 获取jvm数据 114 | send_data['command'] = "jvm" 115 | 116 | elif request_data['type'] == 'queryClassSource': 117 | # 通过jad命令查看class源码 118 | class_name = request_data['command']['className'] 119 | method_name = request_data['command']['methodName'] 120 | send_data['command'] = "jad %s %s --lineNumber false" % (class_name, method_name) 121 | 122 | else: 123 | return APIResponse(errcode=4003, errmsg='命令执行失败, 失败原因:该接口暂不支持!') 124 | 125 | result = requests.post(url=url, data=json.dumps(send_data), headers=headers) 126 | if result.status_code == 200: 127 | data = demjson.decode(result.content) 128 | logger.info(f'请求的报文: {send_data}, 返回结果: {data}') 129 | return APIResponse(errcode=0, errmsg='获取线程成功', data=data) 130 | else: 131 | return APIResponse(errcode=5004, errmsg='获取线程发生异常,请刷新后再试!') 132 | 133 | else: 134 | return APIResponse(errcode=5005, errmsg='初始化会话失败, 请刷新后再试!') 135 | except BaseException as e: 136 | print(e) 137 | logger.error(f'系统发生异常, 异常原因:{e}') 138 | return APIResponse(errcode=5006, errmsg='系统发生异常, 请刷新后再试!') 139 | 140 | 141 | class ListServiceNameView(ListAPIView): 142 | authentication_classes = [MyAuthentication] 143 | permission_classes = [MyPermission, ] 144 | serializer_class = DiagnosisSerializer 145 | 146 | def get_queryset(self): 147 | """列出应用诊断服务名.""" 148 | return Diagnosis.objects.all() 149 | -------------------------------------------------------------------------------- /tasks/application.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2021/5/28 0028 上午 11:18 11 | @Author: micheng. 12 | @File: application.py 13 | """ 14 | 15 | import json 16 | import logging 17 | import time 18 | 19 | import requests 20 | from asgiref.sync import async_to_sync 21 | from celery import shared_task 22 | from channels.layers import get_channel_layer 23 | from django_redis import get_redis_connection 24 | from apps.cmdb.models import Server 25 | from apps.application.models import Diagnosis 26 | 27 | channel_layer = get_channel_layer() 28 | logger = logging.getLogger('default') 29 | 30 | 31 | @shared_task 32 | def app_diagnosis(channel_name, data: dict): 33 | """" 34 | :params agent_id为字典 {"type": "trace", "command": {"trace com.trip"}} 35 | """ 36 | print(data, '字典data') 37 | agent_id = data['command']['agentId'] 38 | queryset = Diagnosis.objects.filter(name=agent_id).values('ip') 39 | trace_thread = TraceThread(agent_id, queryset.ip, data) 40 | result = trace_thread.result() 41 | if result: 42 | logger.info(f'线程追踪完毕: {result}') 43 | async_to_sync(channel_layer.send)(channel_name, {"type": "arthas", "message": str(result)}) 44 | else: 45 | return False 46 | 47 | 48 | class TraceThread(object): 49 | def __init__(self, agent_id, ip, data): 50 | self.data = data 51 | self.agent_id = agent_id 52 | self.url = f"http://{ip}:8563/api" 53 | self.headers = { 54 | "Content-Type": "application/json;charset=utf-8" 55 | } 56 | self.init = {"action": "init_session"} 57 | self.class_name = data['command']['className'] 58 | self.method_name = data['command']['methodName'] 59 | self.trace_time = data['command']['traceTime'] 60 | self.trace_count = data['command']['traceCount'] 61 | self.trace_filter = data['command']['traceFilter'] 62 | 63 | def _init_session(self, agent_id: str) -> list: 64 | self.agent_id = agent_id 65 | 66 | """ 67 | :params agent_id 诊断客户端名称 68 | """ 69 | 70 | # 获取session_id、 consumer_id, 如果过期重新初始化会话 71 | conn_redis = get_redis_connection('default') 72 | agent_session_key = conn_redis.get(self.agent_id) 73 | if agent_session_key is None: 74 | try: 75 | response = requests.post(self.url, data=json.dumps(self.init), headers=self.headers, timeout=5) 76 | if response.status_code == 200: 77 | data = json.loads(response.content) 78 | consumer_id = data['consumerId'] 79 | session_id = data['sessionId'] 80 | conn_redis.set(self.agent_id, session_id + "_" + consumer_id) 81 | return [session_id, consumer_id] 82 | else: 83 | logger.error('初始化会话失败, 请刷新后再试') 84 | return [] 85 | except BaseException as e: 86 | print(e) 87 | logger.error('session_id过期, 重新初始化会话失败!') 88 | return [] 89 | else: 90 | return agent_session_key.split("_") 91 | 92 | def result(self): 93 | # 判断初始化session是否成功 94 | if not TraceThread._init_session(self, agent_id=self.agent_id): 95 | return False 96 | 97 | else: 98 | session_id, consumer_id = TraceThread._init_session(self, agent_id=self.agent_id) 99 | skip_jdk_method = '--skipJDKMethod false' 100 | async_data = { 101 | "action": "async_exec", 102 | "command": None, 103 | "sessionId": session_id 104 | } 105 | if self.trace_time and self.trace_count and self.trace_filter: 106 | async_data["command"] = "trace %s %s '%s' %s %s" % (self.class_name, self.method_name, 107 | '#cost > 5', '-n 20', skip_jdk_method) 108 | elif self.trace_count and self.trace_filter: 109 | async_data["command"] = "trace %s %s %s %s" % (self.class_name, self.method_name, 110 | '-n 20', skip_jdk_method) 111 | 112 | elif self.trace_time and self.trace_filter: 113 | async_data["command"] = "trace %s %s '%s' %s" % (self.class_name, self.method_name, 114 | '#cost > 5', skip_jdk_method), 115 | 116 | elif self.trace_time and self.trace_count: 117 | async_data["command"] = "trace %s %s '%s' %s" %(self.class_name, self.method_name, '#cost > 5', '-n 20') 118 | 119 | elif self.trace_time: 120 | async_data["command"] = "trace %s %s -n 5" % (self.class_name, self.method_name) 121 | 122 | elif self.trace_count: 123 | async_data["command"] = "trace %s %s %s" % (self.class_name, self.method_name, '-n 20') 124 | 125 | elif self.trace_filter: 126 | async_data["command"] = "trace %s %s %s" % (self.class_name, self.method_name, skip_jdk_method) 127 | 128 | else: 129 | async_data["command"] = "trace %s %s '%s'" % (self.class_name, self.method_name, '-n 50') 130 | 131 | print('发送的数据:%s' % async_data) 132 | requests.post(url=self.url, data=json.dumps(async_data), headers=self.headers, timeout=5) 133 | 134 | while True: 135 | pull_data = { 136 | "action": "pull_results", 137 | "sessionId": session_id, 138 | "consumerId": consumer_id 139 | } 140 | 141 | rsp = requests.post(url=self.url, data=json.dumps(pull_data), headers=self.headers) 142 | time.sleep(1) 143 | try: 144 | if rsp.status_code == 200: 145 | data = json.loads(rsp.content) 146 | return data 147 | 148 | except BaseException as e: 149 | print(e) 150 | logger.error(f'获取异步任务结果异常!{e}') 151 | return False 152 | 153 | 154 | @shared_task 155 | def install_agent(app_name: str, ip: str): 156 | """ 157 | :params app_name 应用名 158 | :params ip 应用ip 159 | """ 160 | # 获取arthas安装包 161 | # 判断art心跳是否正常, 正常则不安装 162 | pass -------------------------------------------------------------------------------- /doc/v2_init_sql/rbac_permission.sql: -------------------------------------------------------------------------------- 1 | SET FOREIGN_KEY_CHECKS=0; 2 | 3 | -- ---------------------------- 4 | -- Table structure for rbac_permission 5 | -- ---------------------------- 6 | DROP TABLE IF EXISTS `rbac_permission`; 7 | CREATE TABLE `rbac_permission` ( 8 | `id` int(11) NOT NULL AUTO_INCREMENT, 9 | `name` varchar(32) NOT NULL, 10 | `path` varchar(128) NOT NULL, 11 | `method` varchar(16) NOT NULL, 12 | `pid_id` int(11) DEFAULT NULL, 13 | PRIMARY KEY (`id`), 14 | UNIQUE KEY `name` (`name`), 15 | KEY `rbac_permission_pid_id_6939354d_fk_rbac_permission_id` (`pid_id`), 16 | CONSTRAINT `rbac_permission_pid_id_6939354d_fk_rbac_permission_id` FOREIGN KEY (`pid_id`) REFERENCES `rbac_permission` (`id`) 17 | ) ENGINE=InnoDB AUTO_INCREMENT=57 DEFAULT CHARSET=utf8; 18 | 19 | -- ---------------------------- 20 | -- Records of rbac_permission 21 | -- ---------------------------- 22 | INSERT INTO `rbac_permission` VALUES ('1', '菜单', '/api/rbac/menu/tree/', 'GET', null); 23 | INSERT INTO `rbac_permission` VALUES ('2', '资产管理', '', 'GET', null); 24 | INSERT INTO `rbac_permission` VALUES ('3', '查看服务器列表', '/api/cmdb/(?P[v1|v2]+)/server', 'GET', '2'); 25 | INSERT INTO `rbac_permission` VALUES ('4', '查看服务器详情', '/api/cmdb/(?P[v1|v2]+)/server/(?P\\w+.*)$', 'GET', '2'); 26 | INSERT INTO `rbac_permission` VALUES ('5', '作业配置', '', 'GET', null); 27 | INSERT INTO `rbac_permission` VALUES ('6', '查看执行任务列表', '/api/cmdb/(?P[v1|v2]+)/ansible/tasks/list$', 'GET', '5'); 28 | INSERT INTO `rbac_permission` VALUES ('7', '查看文件分发列表', '/api/cmdb/(?P[v1|v2]+)/ansible/send_list$', 'GET', '5'); 29 | INSERT INTO `rbac_permission` VALUES ('8', '获取执行命令机器列表', '/api/cmdb/(?P[v1|v2]+)/ansible/server$', 'GET', '5'); 30 | INSERT INTO `rbac_permission` VALUES ('9', '获取执行命令模板列表', '/api/cmdb/(?P[v1|v2]+)/ansible/template/list$', 'GET', '5'); 31 | INSERT INTO `rbac_permission` VALUES ('10', '文件分发上传接口', '/api/cmdb/v1/uploads', 'POST', '5'); 32 | INSERT INTO `rbac_permission` VALUES ('11', '执行命令下发', '/api/cmdb/(?P[v1|v2]+)/ansible/execute$', 'POST', '5'); 33 | INSERT INTO `rbac_permission` VALUES ('12', '查看命令执行详情', '/api/cmdb/(?P[v1|v2]+)/ansible/tasks/list$', 'POST', '5'); 34 | INSERT INTO `rbac_permission` VALUES ('13', '执行文件分发命令', '/api/cmdb/(?P[v1|v2]+)/ansible/sendfile$', 'POST', '5'); 35 | INSERT INTO `rbac_permission` VALUES ('14', '创建任务执行模板', '/api/cmdb/(?P[v1|v2]+)/ansible/template/list$', 'POST', '5'); 36 | INSERT INTO `rbac_permission` VALUES ('15', '命令执行结果搜索', '/api/cmdb/(?P[v1|v2]+)/ansible/execute/search', 'GET', '5'); 37 | INSERT INTO `rbac_permission` VALUES ('16', '文件分发结果搜索', '/api/cmdb/(?P[v1|v2]+)/ansible/sendfile/search', 'GET', '5'); 38 | INSERT INTO `rbac_permission` VALUES ('17', '任务执行模板搜索', '/api/cmdb/(?P[v1|v2]+)/ansible/template/search', 'GET', '5'); 39 | INSERT INTO `rbac_permission` VALUES ('18', '删除任务执行模板', '/api/cmdb/(?P[v1|v2]+)/ansible/template/list', 'DELETE', '5'); 40 | INSERT INTO `rbac_permission` VALUES ('19', '容器管理', '', 'GET', null); 41 | INSERT INTO `rbac_permission` VALUES ('20', '执行命令主机搜索', '/api/cmdb/(?P[v1|v2]+)/ansible/searchHost$', 'GET', '5'); 42 | INSERT INTO `rbac_permission` VALUES ('21', '读取任务执行模板内容', '/api/cmdb/(?P[v1|v2]+)/ansible/template/read', 'GET', '5'); 43 | INSERT INTO `rbac_permission` VALUES ('22', '获取节点池', '/api/k8s/(?P[v1|v2]+)/nodes$', 'GET', '19'); 44 | INSERT INTO `rbac_permission` VALUES ('23', '查看Node节点详情', '/api/k8s/(?P[v1|v2]+)/detail/node', 'GET', '19'); 45 | INSERT INTO `rbac_permission` VALUES ('24', '获取Node事件信息', '/api/k8s/v1/events/node', 'GET', '19'); 46 | INSERT INTO `rbac_permission` VALUES ('25', '计算Node节点Pod数量', '/api/k8s/(?P[v1|v2]+)/pods/node$', 'GET', '19'); 47 | INSERT INTO `rbac_permission` VALUES ('26', '查看监控指标', '/api/k8s/(?P[v1|v2]+)/metrics/node$', 'GET', '19'); 48 | INSERT INTO `rbac_permission` VALUES ('27', '查看Pod详情', '/api/k8s/(?P[v1|v2]+)/pod$', 'GET', '19'); 49 | INSERT INTO `rbac_permission` VALUES ('28', '获取Pod事件信息', '/api/k8s/(?P[v1|v2]+)/event/pod$', 'GET', '19'); 50 | INSERT INTO `rbac_permission` VALUES ('29', '编辑Pod Yaml文件', '/api/k8s/(?P[v1|v2]+)/pod$', 'PUT', '19'); 51 | INSERT INTO `rbac_permission` VALUES ('30', '查看Pod日志', '/api/k8s/(?P[v1|v2]+)/logs$', 'POST', '19'); 52 | INSERT INTO `rbac_permission` VALUES ('31', '删除Pod', '/api/k8s/(?P[v1|v2]+)/pod$', 'DELETE', '19'); 53 | INSERT INTO `rbac_permission` VALUES ('32', '设置Node不可调度', '/api/k8s/(?P[v1|v2]+)/nodes$', 'PUT', '19'); 54 | INSERT INTO `rbac_permission` VALUES ('33', '设置Node节点排水', '/api/k8s/(?P[v1|v2]+)/drain/nodes$', 'GET', '19'); 55 | INSERT INTO `rbac_permission` VALUES ('34', '移除Node节点', '/api/k8s/(?P[v1|v2]+)/nodes$', 'DELETE', '19'); 56 | INSERT INTO `rbac_permission` VALUES ('35', '获取无状态应用列表', '/api/k8s/(?P[v1|v2]+)/deployments$', 'GET', '19'); 57 | INSERT INTO `rbac_permission` VALUES ('36', '获取命名空间', '/api/k8s/(?P[v1|v2]+)/namespaces$', 'GET', '19'); 58 | INSERT INTO `rbac_permission` VALUES ('37', '获取容器组', '/api/k8s/(?P[v1|v2]+)/pods/list$', 'GET', '19'); 59 | INSERT INTO `rbac_permission` VALUES ('38', '修改个人密码', '/api/account/(?P[v1|v2]+)/user/password/change$', 'POST', '39'); 60 | INSERT INTO `rbac_permission` VALUES ('39', '用户管理', '', 'GET', null); 61 | INSERT INTO `rbac_permission` VALUES ('40', '查看用户中心', '/api/account/(?P[v1|v2]+)/users$', 'GET', '39'); 62 | INSERT INTO `rbac_permission` VALUES ('41', '查看用户角色', '/api/rbac/roles/', 'GET', '39'); 63 | INSERT INTO `rbac_permission` VALUES ('42', '修改用户角色', '/api/account/(?P[v1|v2]+)/user/role$', 'PUT', '39'); 64 | INSERT INTO `rbac_permission` VALUES ('43', '禁用用户', '/api/account/(?P[v1|v2]+)/users$', 'PUT', '39'); 65 | INSERT INTO `rbac_permission` VALUES ('44', '删除用户', '/api/account/(?P[v1|v2]+)/users$', 'DELETE', '39'); 66 | INSERT INTO `rbac_permission` VALUES ('45', '添加角色', '/api/rbac/roles/', 'POST', '39'); 67 | INSERT INTO `rbac_permission` VALUES ('46', '修改角色', '/api/rbac/roles/(.*)', 'PUT', '39'); 68 | INSERT INTO `rbac_permission` VALUES ('47', '删除角色', '/api/rbac/roles/(.*)', 'DELETE', '39'); 69 | INSERT INTO `rbac_permission` VALUES ('48', '应用诊断', '', 'GET', null); 70 | INSERT INTO `rbac_permission` VALUES ('49', '查看线程清单', '/api/application/(?P[v1|v2]+)/diagnosis', 'POST', '48'); 71 | INSERT INTO `rbac_permission` VALUES ('50', '查看首页仪表盘统计数据', '/api/cmdb/(?P[v1|v2]+)/dashboard/count$', 'GET', null); 72 | INSERT INTO `rbac_permission` VALUES ('51', '查看诊断服务', '/api/application/v1/diagnosis/service/list', 'GET', '48'); 73 | INSERT INTO `rbac_permission` VALUES ('52', 'Deployment扩缩容', '/api/k8s/(?P[v1|v2]+)/deployment/scale$', 'POST', '19'); 74 | INSERT INTO `rbac_permission` VALUES ('53', '查看Deployment详情', '/api/k8s/(?P[v1|v2]+)/deployment/detail$', 'GET', '19'); 75 | INSERT INTO `rbac_permission` VALUES ('54', '获取Deployment历史版本', '/api/k8s/(?P[v1|v2]+)/deployment/history$', 'GET', '19'); 76 | INSERT INTO `rbac_permission` VALUES ('55', '创建Deployment回滚', '/api/k8s/(?P[v1|v2]+)/deployment/history$', 'POST', '19'); 77 | INSERT INTO `rbac_permission` VALUES ('56', '查看Deployment事件', '/api/k8s/(?P[v1|v2]+)/event/deployment$', 'GET', '19'); 78 | -------------------------------------------------------------------------------- /apps/k8s/views/pod.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) SmallFlyingPigs Organization. https://github.com/small-flying-pigs 5 | # Copyright: (c) 6 | # Released under the AGPL-3.0 License. 7 | 8 | 9 | """ 10 | @Time: 2020/12/2 10:28 11 | @Author: yu.jiang 12 | @License: Apache License 13 | @File: pod.py 14 | """ 15 | 16 | import json 17 | import yaml 18 | import logging 19 | import traceback 20 | 21 | from kubernetes.client import ApiException 22 | from kubernetes import client, config 23 | from rest_framework.views import APIView 24 | from rest_framework.response import Response 25 | 26 | from apps.account.models import User 27 | from apps.k8s.serializers.pod import PodSerializers 28 | from utils.authorization import MyAuthentication 29 | from utils.http_response import APIResponse 30 | from utils.pagination import MyPageNumberPagination 31 | from utils.permissions import MyPermission 32 | 33 | logger = logging.getLogger('default') 34 | config.load_kube_config() 35 | v1 = client.CoreV1Api() 36 | 37 | 38 | class PodViews(APIView): 39 | """ 40 | Pod List 41 | """ 42 | authentication_classes = [MyAuthentication] 43 | permission_classes = [MyPermission, ] 44 | 45 | def get(self, request, *args, **kwargs): 46 | namespace = request.query_params.dict().get('namespace', 'default') 47 | try: 48 | ret = v1.list_namespaced_pod(namespace=namespace) 49 | tmp_context = [] 50 | for i in ret.items: 51 | tmp_dict = dict() 52 | tmp_dict['pod_ip'] = i.status.pod_ip 53 | tmp_dict['namespace'] = i.metadata.namespace 54 | from django.utils import timezone 55 | tmp_dict['create_time'] = timezone.localtime(i.metadata.creation_timestamp).strftime("%Y-%m-%d %H:%M:%S") 56 | tmp_dict['name'] = i.metadata.name 57 | tmp_dict['host_ip'] = i.status.host_ip 58 | tmp_dict['status'] = i.status.to_dict() 59 | # 当pod处于Pending状态, 此时pod重启次数 i.status.phase 信息为 None 60 | # 如果为None, 则返回 0 61 | tmp_dict['restart_count'] = [0 if i.status.container_statuses is None else i.status.container_statuses[0].restart_count][0] 62 | tmp_context.append(tmp_dict) 63 | paginator = MyPageNumberPagination() 64 | page_publish_list = paginator.paginate_queryset(tmp_context, self.request, view=self) 65 | ps = PodSerializers(page_publish_list, many=True) 66 | response = paginator.get_paginated_response(ps.data) 67 | return APIResponse(data=response.data) 68 | 69 | except ApiException as e: 70 | logger.error(e, str(traceback.format_exc())) 71 | return APIResponse(errcode=e.status, errmsg=e.body) 72 | except Exception as e: 73 | logger.error(e, str(traceback.format_exc())) 74 | return APIResponse(errcode=1000, errmsg='获取pod信息出错') 75 | 76 | 77 | class DetailPodView(APIView): 78 | """ 79 | 对具体的Pod操作 80 | """ 81 | authentication_classes = [MyAuthentication] 82 | permission_classes = [MyPermission, ] 83 | 84 | def get(self, request, *args, **kwargs): 85 | name = request.query_params.dict().get('pod_name') 86 | namespace = request.query_params.dict().get('namespace', 'default') 87 | try: 88 | ret = v1.read_namespaced_pod(name=name, namespace=namespace) 89 | pod_yaml = yaml.safe_dump(ret.to_dict()) 90 | ret_tmp = {} 91 | ret_tmp = ret.to_dict() 92 | ret_tmp['pod_yaml'] = str(pod_yaml) 93 | return APIResponse(data=ret_tmp) 94 | 95 | except ApiException as e: 96 | logger.error(e, str(traceback.format_exc())) 97 | return APIResponse(errcode=e.status, errmsg=e.body) 98 | except Exception as e: 99 | logger.error(e, str(traceback.format_exc())) 100 | return APIResponse(errcode=9999, errmsg='获取Pod信息失败') 101 | 102 | def delete(self, request, *args, **kwargs): 103 | name = request.query_params.dict().get('name') 104 | namespace = request.query_params.dict().get('namespace', 'default') 105 | # 允许可删除命名空间下的pod 106 | # allow_delete_namespace_in_pod = ['develop', 'release', 'uat'] 107 | user_obj = User.objects.filter(username=request.user.username).first() 108 | user_group = user_obj.roles.all() 109 | 110 | logger.info('用户:%s, 请求删除pod: %s, 命名空间:%s' % (str(request.user.username), name, namespace)) 111 | if not request.user.is_superuser: 112 | if user_group: 113 | group = str(user_group[0]).strip() 114 | if group == 'develop' and namespace != 'develop' or namespace != 'dingtalk': 115 | return APIResponse(errcode=403, errmsg='无权限删除') 116 | elif group == 'test' and namespace != 'release' or namespace != 'dingtalk': 117 | return APIResponse(errcode=403, errmsg='无权限删除') 118 | else: 119 | return APIResponse(errcode=403, errmsg='无权限删除') 120 | else: 121 | return APIResponse(errcode=403, errmsg='无权限删除') 122 | 123 | 124 | try: 125 | ret = v1.delete_namespaced_pod(name=name, namespace=namespace) 126 | if ret.status is None: 127 | return APIResponse(data=ret.to_dict()) 128 | 129 | except ApiException as e: 130 | logger.error(e, str(traceback.format_exc())) 131 | return APIResponse(errcode=e.status, errmsg=e.body) 132 | except Exception as e: 133 | logger.error(e, str(traceback.format_exc())) 134 | return APIResponse(errcode=9999, errmsg='删除失败') 135 | 136 | def put(self, request, *args, **kwargs): 137 | data = request.data['params'] 138 | if request.data.get('body') is None: 139 | return APIResponse(errcode=400, errmsg='YAML不能为空') 140 | 141 | logger.info("patch pod:%s" % data) 142 | body = yaml.safe_load(request.data.get('body')) 143 | try: 144 | res = v1.patch_namespaced_pod(namespace=data.get('namespace'), name=data.get('pod_name'), body=body) 145 | return APIResponse(data=res.to_dict()) 146 | except ApiException as e: 147 | logger.error("pod更新异常", str(traceback.format_exc())) 148 | return APIResponse(errcode=e.status, errmsg=str(e.body).encode('utf-8').decode('unicode_escape')) 149 | except Exception as e: 150 | logger.error('更新pod失败:%s' % str(traceback.format_exc())) 151 | return APIResponse(errcode=9999, errmsg='更新pod失败') 152 | 153 | def post(self, request, *args, **kwargs): 154 | context = {'errcode': 0, 'msg': '部署成功!', 'data': ''} 155 | namespace = request.data.get('namespace', 'default') 156 | try: 157 | body = yaml.safe_load(request.data.get('body')) 158 | deploy_type = body.get('kind') 159 | if deploy_type != 'Pod': 160 | context['errcode'] = 1000 161 | context['msg'] = '部署类型错误!' 162 | return Response(context) 163 | except Exception as e: 164 | logger.error('部署pod异常:%s' % str(traceback.format_exc())) 165 | context['errcode'] = 1000 166 | context['msg'] = e 167 | return Response(context) 168 | 169 | try: 170 | v1.create_namespaced_pod(namespace=namespace, body=body) 171 | except Exception as e: 172 | logger.error('部署pod异常:%s' % str(traceback.format_exc())) 173 | context['errcode'] = 1000 174 | context['msg'] = e 175 | return Response(context) 176 | 177 | 178 | class PodFromNodeView(APIView): 179 | 180 | authentication_classes = [MyAuthentication] 181 | permission_classes = [MyPermission, ] 182 | 183 | def get(self, request, *args, **kwargs): 184 | """ 185 | 查看Node节点部署了多少个Pod 186 | """ 187 | try: 188 | node_name = request.query_params 189 | app_v1 = client.CoreV1Api() 190 | logger.info("List k8s nodes to pods...") 191 | field_selector = ("spec.nodeName=" + node_name.get("name")) 192 | res = app_v1.list_pod_for_all_namespaces(field_selector=field_selector) 193 | return APIResponse(data=res.to_dict()) 194 | except ApiException as e: 195 | logger.error(e, str(traceback.format_exc())) 196 | return APIResponse(errcode=e.status, errmsg=e.body) 197 | except BaseException as e: 198 | logger.error(e, str(traceback.format_exc())) 199 | return APIResponse(errcode=9999, errmsg='服务异常') --------------------------------------------------------------------------------