├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ └── github_to_gitee.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── FQA.md ├── docker │ ├── Dockerfile │ ├── docker-compose.yml │ ├── entrypoint.sh │ ├── init_spug │ ├── nginx.conf │ ├── redis.conf │ ├── spug.ini │ └── ssh_config └── install.sh ├── spug_api ├── .gitignore ├── apps │ ├── account │ │ ├── __init__.py │ │ ├── history.py │ │ ├── management │ │ │ └── commands │ │ │ │ ├── set.py │ │ │ │ ├── update.py │ │ │ │ ├── updatedb.py │ │ │ │ └── user.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── alarm │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── apis │ │ ├── __init__.py │ │ ├── config.py │ │ ├── deploy.py │ │ └── urls.py │ ├── app │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── config │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── deploy │ │ ├── __init__.py │ │ ├── helper.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── exec │ │ ├── __init__.py │ │ ├── executors.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── runworker.py │ │ ├── models.py │ │ ├── transfer.py │ │ ├── urls.py │ │ └── views.py │ ├── file │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── home │ │ ├── __init__.py │ │ ├── models.py │ │ ├── navigation.py │ │ ├── notice.py │ │ ├── urls.py │ │ └── views.py │ ├── host │ │ ├── __init__.py │ │ ├── add.py │ │ ├── extend.py │ │ ├── group.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── monitor │ │ ├── __init__.py │ │ ├── executors.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── runmonitor.py │ │ ├── models.py │ │ ├── scheduler.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── notify │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ └── views.py │ ├── repository │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── schedule │ │ ├── __init__.py │ │ ├── builtin.py │ │ ├── executors.py │ │ ├── management │ │ │ └── commands │ │ │ │ └── runscheduler.py │ │ ├── models.py │ │ ├── scheduler.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ └── setting │ │ ├── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── user.py │ │ ├── utils.py │ │ └── views.py ├── consumer │ ├── __init__.py │ ├── consumers.py │ ├── routing.py │ └── utils.py ├── libs │ ├── __init__.py │ ├── channel.py │ ├── decorators.py │ ├── gitlib.py │ ├── helper.py │ ├── ldap.py │ ├── mail.py │ ├── middleware.py │ ├── mixins.py │ ├── parser.py │ ├── push.py │ ├── spug.py │ ├── ssh.py │ ├── utils.py │ └── validators.py ├── logs │ └── .gitkeep ├── manage.py ├── repos │ ├── .gitkeep │ └── build │ │ └── .gitkeep ├── requirements.txt ├── spug │ ├── __init__.py │ ├── asgi.py │ ├── routing.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── storage │ └── transfer │ │ └── .gitkeep └── tools │ ├── migrate.py │ ├── start-api.sh │ ├── start-monitor.sh │ ├── start-scheduler.sh │ ├── start-worker.sh │ ├── start-ws.sh │ └── supervisor-spug.ini └── spug_web ├── .gitignore ├── README.md ├── config-overrides.js ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.png ├── manifest.json ├── resource │ ├── gitee.png │ ├── gitlab.png │ ├── grafana.png │ ├── prometheus.png │ ├── wiki.png │ └── 主机导入模板.xlsx └── robots.txt └── src ├── App.js ├── components ├── 404.svg ├── ACEditor.js ├── Action.js ├── AppSelector.js ├── AuthButton.js ├── AuthCard.js ├── AuthDiv.js ├── AuthFragment.js ├── Breadcrumb.js ├── Link.js ├── LinkButton.js ├── NotFound.js ├── SearchForm.js ├── StatisticsCard.js ├── TableCard.js ├── index.js └── index.module.less ├── gStore.js ├── index.js ├── index.less ├── layout ├── Footer.js ├── Header.js ├── Notification.js ├── Sider.js ├── avatar.png ├── index.js ├── layout.module.less ├── logo-spug-txt.png ├── logo-spug-white.png └── logo-spug.png ├── libs ├── 404.svg ├── functools.js ├── history.js ├── http.js ├── index.js ├── libs.module.css └── router.js ├── pages ├── alarm │ ├── alarm │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ ├── contact │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ └── group │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js ├── config │ ├── app │ │ ├── Form.js │ │ ├── Rel.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ ├── environment │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ ├── service │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ └── setting │ │ ├── DiffConfig.js │ │ ├── Form.js │ │ ├── JSONView.js │ │ ├── Record.js │ │ ├── TableView.js │ │ ├── TextView.js │ │ ├── index.js │ │ ├── index.module.css │ │ └── store.js ├── dashboard │ ├── AlarmTrend.js │ ├── RequestTop.js │ ├── StatisticCard.js │ ├── index.js │ └── index.module.css ├── deploy │ ├── app │ │ ├── AddSelect.js │ │ ├── AutoDeploy.js │ │ ├── CloneConfirm.js │ │ ├── Ext1Form.js │ │ ├── Ext1Setup1.js │ │ ├── Ext1Setup2.js │ │ ├── Ext1Setup3.js │ │ ├── Ext2Form.js │ │ ├── Ext2Setup1.js │ │ ├── Ext2Setup2.js │ │ ├── Form.js │ │ ├── Repo.js │ │ ├── Table.js │ │ ├── Tips.js │ │ ├── index.js │ │ ├── index.module.css │ │ └── store.js │ ├── repository │ │ ├── Console.js │ │ ├── Detail.js │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ ├── index.module.less │ │ └── store.js │ └── request │ │ ├── Approve.js │ │ ├── BatchDelete.js │ │ ├── Ext1Console.js │ │ ├── Ext1Form.js │ │ ├── Ext2Console.js │ │ ├── Ext2Form.js │ │ ├── HostSelector.js │ │ ├── OutView.js │ │ ├── Rollback.js │ │ ├── Table.js │ │ ├── index.js │ │ ├── index.module.less │ │ └── store.js ├── exec │ ├── task │ │ ├── Output.js │ │ ├── Parameter.js │ │ ├── TemplateSelector.js │ │ ├── index.js │ │ ├── index.module.less │ │ └── store.js │ ├── template │ │ ├── Form.js │ │ ├── Parameter.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ └── transfer │ │ ├── Output.js │ │ ├── index.js │ │ ├── index.module.less │ │ └── store.js ├── home │ ├── Nav.js │ ├── NavForm.js │ ├── Notice.js │ ├── Todo.js │ ├── index.js │ └── index.module.less ├── host │ ├── BatchSync.js │ ├── CloudImport.js │ ├── Detail.js │ ├── Form.js │ ├── Group.js │ ├── IPAddress.js │ ├── Import.js │ ├── Selector.js │ ├── Sync.js │ ├── Table.js │ ├── icons │ │ ├── alibaba.png │ │ ├── centos.png │ │ ├── coreos.png │ │ ├── debian.png │ │ ├── excel.png │ │ ├── fedora.png │ │ ├── freebsd.png │ │ ├── index.js │ │ ├── linux.png │ │ ├── suse.png │ │ ├── tencent.png │ │ ├── ubuntu.png │ │ └── windows.png │ ├── index.js │ ├── index.module.less │ ├── selector.module.less │ ├── store.js │ └── store2.js ├── login │ ├── bg.svg │ ├── index.js │ └── login.module.css ├── monitor │ ├── Form.js │ ├── MonitorCard.js │ ├── Step1.js │ ├── Step2.js │ ├── Table.js │ ├── index.js │ ├── index.module.less │ └── store.js ├── schedule │ ├── Form.js │ ├── Info.js │ ├── Record.js │ ├── Step1.js │ ├── Step2.js │ ├── Step3.js │ ├── Table.js │ ├── index.js │ ├── index.module.css │ └── store.js ├── ssh │ ├── FileManager.js │ ├── Setting.js │ ├── Terminal.js │ ├── index.js │ ├── index.module.less │ ├── setting.module.less │ └── themes.js ├── system │ ├── account │ │ ├── Form.js │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ ├── login │ │ ├── Table.js │ │ ├── index.js │ │ └── store.js │ ├── role │ │ ├── DeployPerm.js │ │ ├── Form.js │ │ ├── HostPerm.js │ │ ├── PagePerm.js │ │ ├── RoleUsers.js │ │ ├── Table.js │ │ ├── codes.js │ │ ├── index.js │ │ ├── index.module.css │ │ └── store.js │ └── setting │ │ ├── About.js │ │ ├── AlarmSetting.js │ │ ├── KeySetting.js │ │ ├── LDAPSetting.js │ │ ├── OpenService.js │ │ ├── PushSetting.js │ │ ├── SecuritySetting.js │ │ ├── index.js │ │ ├── index.module.css │ │ └── store.js └── welcome │ ├── index │ └── index.js │ └── info │ ├── Basic.js │ ├── Reset.js │ ├── index.js │ ├── index.module.css │ └── store.js ├── routes.js ├── serviceWorker.js └── setupProxy.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.spug.dev/sponsorship/'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: Report a reproducible bug or regression. 4 | title: 'Bug: ' 5 | --- 6 | 7 | 10 | 11 | Spug 版本: 12 | 13 | ## 问题重现步骤 14 | 15 | 1. 16 | 2. 17 | 18 | ## 报错/问题截图 19 | 20 | 21 | ## 期望的结果 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/github_to_gitee.yml: -------------------------------------------------------------------------------- 1 | name: github repos to gitee job 2 | on: 3 | # 如果需要PR触发把push前的#去掉 4 | # push: 5 | schedule: 6 | # 每天北京时间1点跑 7 | - cron: '0 1 * * *' 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Mirror the Github organization repos to Gitee. 13 | uses: Yikun/gitee-mirror-action@v0.01 14 | with: 15 | src: github/openspug 16 | dst: gitee/openspug 17 | # Gitee对应的秘钥 18 | private_key: ${{ secrets.mac_pro_videojj }} 19 | # 需要同步的Github组织名(源) 20 | github_org: openspug 21 | # 需要同步到的Gitee的组织名(目的) 22 | gitee_org: openspug 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /docs/FQA.md: -------------------------------------------------------------------------------- 1 | ### install mysqlclient 2 | ```shell 3 | # for centos 7 4 | yum install mariadb-devel python3-devel gcc 5 | pip install mysqlclient 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7.9.2009 2 | 3 | ENV TZ=Asia/Shanghai 4 | RUN yum install -y epel-release https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm && yum install -y --setopt=tsflags=nodocs nginx redis mariadb-devel python36 python36-devel openldap-devel supervisor git gcc wget unzip net-tools sshpass rsync sshfs && yum -y clean all --enablerepo='*' 5 | 6 | RUN pip3 install --no-cache-dir --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ 7 | RUN pip3 install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ \ 8 | gunicorn \ 9 | mysqlclient \ 10 | cryptography==36.0.2 \ 11 | apscheduler==3.7.0 \ 12 | asgiref==3.2.10 \ 13 | Django==2.2.28 \ 14 | channels==2.3.1 \ 15 | channels_redis==2.4.1 \ 16 | paramiko==2.11.0 \ 17 | django-redis==4.10.0 \ 18 | requests==2.22.0 \ 19 | GitPython==3.0.8 \ 20 | python-ldap==3.4.0 \ 21 | openpyxl==3.0.3 \ 22 | user_agents==2.2.0 23 | 24 | RUN localedef -c -i en_US -f UTF-8 en_US.UTF-8 25 | ENV LANG=en_US.UTF-8 26 | ENV LC_ALL=en_US.UTF-8 27 | RUN echo -e '\n# Source definitions\n. /etc/profile\n' >> /root/.bashrc 28 | RUN mkdir -p /data/repos 29 | COPY init_spug /usr/bin/ 30 | COPY nginx.conf /etc/nginx/ 31 | COPY ssh_config /etc/ssh/ 32 | COPY spug.ini /etc/supervisord.d/ 33 | COPY redis.conf /etc/ 34 | COPY entrypoint.sh / 35 | 36 | VOLUME /data 37 | EXPOSE 80 38 | ENTRYPOINT ["/entrypoint.sh"] 39 | -------------------------------------------------------------------------------- /docs/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: mariadb:10.8 5 | container_name: spug-db 6 | restart: always 7 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 8 | volumes: 9 | - /data/spug/mysql:/var/lib/mysql 10 | environment: 11 | - MYSQL_DATABASE=spug 12 | - MYSQL_USER=spug 13 | - MYSQL_PASSWORD=spug.cc 14 | - MYSQL_ROOT_PASSWORD=spug.cc 15 | spug: 16 | image: openspug/spug-service 17 | container_name: spug 18 | privileged: true 19 | restart: always 20 | volumes: 21 | - /data/spug/service:/data/spug 22 | - /data/spug/repos:/data/repos 23 | ports: 24 | # 如果80端口被占用可替换为其他端口,例如: - "8000:80" 25 | - "80:80" 26 | environment: 27 | - SPUG_DOCKER_VERSION=v3.2.4 28 | - MYSQL_DATABASE=spug 29 | - MYSQL_USER=spug 30 | - MYSQL_PASSWORD=spug.cc 31 | - MYSQL_HOST=db 32 | - MYSQL_PORT=3306 33 | depends_on: 34 | - db 35 | -------------------------------------------------------------------------------- /docs/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | set -e 4 | 5 | if [ -e /root/.bashrc ]; then 6 | source /root/.bashrc 7 | fi 8 | 9 | if [ ! -d /data/spug/spug_api ]; then 10 | git clone -b $SPUG_DOCKER_VERSION https://gitee.com/openspug/spug.git /data/spug 11 | curl -o web.tar.gz https://cdn.spug.cc/spug/web_${SPUG_DOCKER_VERSION}.tar.gz 12 | tar xf web.tar.gz -C /data/spug/spug_web/ 13 | rm -f web.tar.gz 14 | SECRET_KEY=$(< /dev/urandom tr -dc '!@#%^.a-zA-Z0-9' | head -c50) 15 | cat > /data/spug/spug_api/spug/overrides.py << EOF 16 | import os 17 | 18 | 19 | DEBUG = False 20 | ALLOWED_HOSTS = ['127.0.0.1'] 21 | SECRET_KEY = '${SECRET_KEY}' 22 | 23 | DATABASES = { 24 | 'default': { 25 | 'ATOMIC_REQUESTS': True, 26 | 'ENGINE': 'django.db.backends.mysql', 27 | 'NAME': os.environ.get('MYSQL_DATABASE'), 28 | 'USER': os.environ.get('MYSQL_USER'), 29 | 'PASSWORD': os.environ.get('MYSQL_PASSWORD'), 30 | 'HOST': os.environ.get('MYSQL_HOST'), 31 | 'PORT': os.environ.get('MYSQL_PORT'), 32 | 'OPTIONS': { 33 | 'charset': 'utf8mb4', 34 | 'sql_mode': 'STRICT_TRANS_TABLES', 35 | } 36 | } 37 | } 38 | EOF 39 | fi 40 | 41 | exec supervisord -c /etc/supervisord.conf 42 | -------------------------------------------------------------------------------- /docs/docker/init_spug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | set -e 4 | set -u 5 | 6 | python3 /data/spug/spug_api/manage.py updatedb 7 | python3 /data/spug/spug_api/manage.py user add -u $1 -p $2 -n 管理员 -s 8 | -------------------------------------------------------------------------------- /docs/docker/spug.ini: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command = nginx -g "daemon off;" 6 | autostart = true 7 | 8 | [program:redis] 9 | command = redis-server /etc/redis.conf 10 | autostart = true 11 | 12 | [program:spug-api] 13 | command = sh /data/spug/spug_api/tools/start-api.sh 14 | autostart = true 15 | stdout_logfile = /data/spug/spug_api/logs/api.log 16 | redirect_stderr = true 17 | 18 | [program:spug-ws] 19 | command = sh /data/spug/spug_api/tools/start-ws.sh 20 | autostart = true 21 | stdout_logfile = /data/spug/spug_api/logs/ws.log 22 | redirect_stderr = true 23 | 24 | [program:spug-worker] 25 | command = sh /data/spug/spug_api/tools/start-worker.sh 26 | autostart = true 27 | stdout_logfile = /data/spug/spug_api/logs/worker.log 28 | redirect_stderr = true 29 | 30 | [program:spug-monitor] 31 | command = sh /data/spug/spug_api/tools/start-monitor.sh 32 | autostart = true 33 | startsecs = 3 34 | stdout_logfile = /data/spug/spug_api/logs/monitor.log 35 | redirect_stderr = true 36 | 37 | [program:spug-scheduler] 38 | command = sh /data/spug/spug_api/tools/start-scheduler.sh 39 | autostart = true 40 | startsecs = 3 41 | stdout_logfile = /data/spug/spug_api/logs/scheduler.log 42 | redirect_stderr = true 43 | -------------------------------------------------------------------------------- /docs/docker/ssh_config: -------------------------------------------------------------------------------- 1 | Host * 2 | StrictHostKeyChecking no 3 | -------------------------------------------------------------------------------- /spug_api/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /venv/ 3 | __pycache__/ 4 | /.idea/ 5 | /db.sqlite3 6 | migrations/ 7 | /access.log 8 | /repos/* 9 | /logs/* 10 | -------------------------------------------------------------------------------- /spug_api/apps/account/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/account/history.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from libs.mixins import AdminView 5 | from libs import json_response 6 | from apps.account.models import History 7 | 8 | 9 | class HistoryView(AdminView): 10 | def get(self, request): 11 | histories = History.objects.all() 12 | return json_response(histories) 13 | -------------------------------------------------------------------------------- /spug_api/apps/account/management/commands/set.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.core.management.base import BaseCommand 5 | from apps.setting.utils import AppSetting 6 | 7 | 8 | class Command(BaseCommand): 9 | help = '系统设置' 10 | 11 | def add_arguments(self, parser): 12 | parser.add_argument('target', type=str, help='设置对象') 13 | parser.add_argument('value', type=str, help='设置值') 14 | 15 | def echo_success(self, msg): 16 | self.stdout.write(self.style.SUCCESS(msg)) 17 | 18 | def echo_error(self, msg): 19 | self.stderr.write(self.style.ERROR(msg)) 20 | 21 | def print_help(self, *args): 22 | message = ''' 23 | 系统设置命令用法: 24 | set mfa disable 禁用登录MFA 25 | ''' 26 | self.stdout.write(message) 27 | 28 | def handle(self, *args, **options): 29 | target = options['target'] 30 | if target == 'mfa': 31 | if options['value'] != 'disable': 32 | return self.echo_error(f'mfa设置,不支持的值【{options["value"]}】') 33 | AppSetting.set('MFA', {'enable': False}) 34 | self.echo_success('MFA已禁用') 35 | else: 36 | self.echo_error('未识别的操作') 37 | self.print_help() 38 | -------------------------------------------------------------------------------- /spug_api/apps/account/management/commands/updatedb.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.core.management.base import BaseCommand 5 | from django.core.management import execute_from_command_line 6 | from django.conf import settings 7 | 8 | 9 | class Command(BaseCommand): 10 | help = '初始化/更新数据库' 11 | 12 | def handle(self, *args, **options): 13 | args = ['manage.py', 'makemigrations'] 14 | apps = [x.split('.')[-1] for x in settings.INSTALLED_APPS if x.startswith('apps.')] 15 | execute_from_command_line(args + apps) 16 | execute_from_command_line(['manage.py', 'migrate']) 17 | self.stdout.write(self.style.SUCCESS('初始化/更新成功')) 18 | -------------------------------------------------------------------------------- /spug_api/apps/account/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.conf.urls import url 5 | 6 | from apps.account.views import * 7 | from apps.account.history import * 8 | 9 | urlpatterns = [ 10 | url(r'^login/$', login), 11 | url(r'^logout/$', logout), 12 | url(r'^user/$', UserView.as_view()), 13 | url(r'^role/$', RoleView.as_view()), 14 | url(r'^self/$', SelfView.as_view()), 15 | url(r'^login/history/$', HistoryView.as_view()) 16 | ] 17 | -------------------------------------------------------------------------------- /spug_api/apps/account/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from apps.host.models import Group 5 | import re 6 | 7 | 8 | def get_host_perms(user): 9 | ids = sub_ids = set(user.group_perms) 10 | while sub_ids: 11 | sub_ids = [x.id for x in Group.objects.filter(parent_id__in=sub_ids)] 12 | ids.update(sub_ids) 13 | return set(x.host_id for x in Group.hosts.through.objects.filter(group_id__in=ids)) 14 | 15 | 16 | def has_host_perm(user, target): 17 | if user.is_supper: 18 | return True 19 | host_ids = get_host_perms(user) 20 | if isinstance(target, (list, set, tuple)): 21 | return set(target).issubset(host_ids) 22 | return int(target) in host_ids 23 | 24 | 25 | def verify_password(password): 26 | if len(password) < 8: 27 | return False 28 | if not all(map(lambda x: re.findall(x, password), ['[0-9]', '[a-z]', '[A-Z]'])): 29 | return False 30 | return True 31 | -------------------------------------------------------------------------------- /spug_api/apps/alarm/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/alarm/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('alarm/', AlarmView.as_view()), 10 | path('group/', GroupView.as_view()), 11 | path('contact/', ContactView.as_view()), 12 | path('test/', handle_test), 13 | ] 14 | -------------------------------------------------------------------------------- /spug_api/apps/apis/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/apis/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from apps.apis import config 7 | from apps.apis import deploy 8 | 9 | urlpatterns = [ 10 | path('config/', config.get_configs), 11 | path('deploy///', deploy.auto_deploy) 12 | ] 13 | -------------------------------------------------------------------------------- /spug_api/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/app/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', AppView.as_view()), 10 | path('kit/key/', kit_key), 11 | path('deploy/', DeployView.as_view()), 12 | path('deploy//versions/', get_versions), 13 | ] 14 | -------------------------------------------------------------------------------- /spug_api/apps/app/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.conf import settings 5 | from apps.app.models import Deploy 6 | from apps.setting.utils import AppSetting 7 | from libs.gitlib import Git 8 | import shutil 9 | import os 10 | 11 | 12 | def parse_envs(text): 13 | data = {} 14 | if text: 15 | for line in text.split('\n'): 16 | fields = line.split('=', 1) 17 | if len(fields) != 2 or fields[0].strip() == '': 18 | raise Exception(f'解析自定义全局变量{line!r}失败,确认其遵循 key = value 格式') 19 | data[fields[0].strip()] = fields[1].strip() 20 | return data 21 | 22 | 23 | def fetch_versions(deploy: Deploy): 24 | git_repo = deploy.extend_obj.git_repo 25 | repo_dir = os.path.join(settings.REPOS_DIR, str(deploy.id)) 26 | pkey = AppSetting.get_default('private_key') 27 | with Git(git_repo, repo_dir, pkey) as git: 28 | return git.fetch_branches_tags() 29 | 30 | 31 | def fetch_repo(deploy_id, git_repo): 32 | repo_dir = os.path.join(settings.REPOS_DIR, str(deploy_id)) 33 | pkey = AppSetting.get_default('private_key') 34 | with Git(git_repo, repo_dir, pkey) as git: 35 | return git.fetch_branches_tags() 36 | 37 | 38 | def remove_repo(deploy_id): 39 | shutil.rmtree(os.path.join(settings.REPOS_DIR, str(deploy_id)), True) 40 | -------------------------------------------------------------------------------- /spug_api/apps/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/config/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', ConfigView.as_view()), 10 | path('parse/json/', parse_json), 11 | path('parse/text/', parse_text), 12 | path('diff/', post_diff), 13 | path('environment/', EnvironmentView.as_view()), 14 | path('service/', ServiceView.as_view()), 15 | path('history/', HistoryView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /spug_api/apps/config/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from apps.config.models import Config, Service 5 | from apps.app.models import App 6 | import json 7 | 8 | 9 | def compose_configs(app, env_id, no_prefix=False): 10 | configs = dict() 11 | # app own configs 12 | for item in Config.objects.filter(type='app', o_id=app.id, env_id=env_id).only('key', 'value'): 13 | key = item.key if no_prefix else f'{app.key}_{item.key}' 14 | configs[key] = item.value 15 | 16 | # relation app public configs 17 | if app.rel_apps: 18 | app_ids = json.loads(app.rel_apps) 19 | if app_ids: 20 | id_key_map = {x.id: x.key for x in App.objects.filter(id__in=app_ids)} 21 | for item in Config.objects.filter(type='app', o_id__in=app_ids, env_id=env_id, is_public=True) \ 22 | .only('key', 'value'): 23 | key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}' 24 | configs[key] = item.value 25 | 26 | # relation service configs 27 | if app.rel_services: 28 | src_ids = json.loads(app.rel_services) 29 | if src_ids: 30 | id_key_map = {x.id: x.key for x in Service.objects.filter(id__in=src_ids)} 31 | for item in Config.objects.filter(type='src', o_id__in=src_ids, env_id=env_id).only('key', 'value'): 32 | key = item.key if no_prefix else f'{id_key_map[item.o_id]}_{item.key}' 33 | configs[key] = item.value 34 | return configs 35 | -------------------------------------------------------------------------------- /spug_api/apps/deploy/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/deploy/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('request/', RequestView.as_view()), 10 | path('request/info/', get_request_info), 11 | path('request/ext1/', post_request_ext1), 12 | path('request/ext1/rollback/', post_request_ext1_rollback), 13 | path('request/ext2/', post_request_ext2), 14 | path('request/upload/', do_upload), 15 | path('request//', RequestDetailView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /spug_api/apps/exec/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/exec/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.conf.urls import url 5 | 6 | from apps.exec.views import * 7 | from apps.exec.transfer import TransferView 8 | 9 | urlpatterns = [ 10 | url(r'template/$', TemplateView.as_view()), 11 | url(r'do/$', TaskView.as_view()), 12 | url(r'transfer/$', TransferView.as_view()), 13 | ] 14 | -------------------------------------------------------------------------------- /spug_api/apps/file/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/file/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', FileView.as_view()), 10 | path('object/', ObjectView.as_view()), 11 | ] 12 | -------------------------------------------------------------------------------- /spug_api/apps/home/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/home/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import models 5 | from libs.mixins import ModelMixin 6 | import json 7 | 8 | 9 | class Notice(models.Model, ModelMixin): 10 | title = models.CharField(max_length=100) 11 | content = models.TextField() 12 | is_stress = models.BooleanField(default=False) 13 | read_ids = models.TextField(default='[]') 14 | sort_id = models.IntegerField(default=0, db_index=True) 15 | created_at = models.DateTimeField(auto_now_add=True) 16 | 17 | def to_view(self): 18 | tmp = self.to_dict() 19 | tmp['read_ids'] = json.loads(self.read_ids) 20 | return tmp 21 | 22 | class Meta: 23 | db_table = 'notices' 24 | ordering = ('-sort_id',) 25 | 26 | 27 | class Navigation(models.Model, ModelMixin): 28 | title = models.CharField(max_length=64) 29 | desc = models.CharField(max_length=128) 30 | logo = models.TextField() 31 | links = models.TextField() 32 | sort_id = models.IntegerField(default=0, db_index=True) 33 | created_at = models.DateTimeField(auto_now_add=True) 34 | 35 | def to_view(self): 36 | tmp = self.to_dict() 37 | tmp['links'] = json.loads(self.links) 38 | return tmp 39 | 40 | class Meta: 41 | db_table = 'navigations' 42 | ordering = ('-sort_id',) 43 | -------------------------------------------------------------------------------- /spug_api/apps/home/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | from apps.home.notice import NoticeView 8 | from apps.home.navigation import NavView 9 | 10 | urlpatterns = [ 11 | path('statistic/', get_statistic), 12 | path('alarm/', get_alarm), 13 | path('deploy/', get_deploy), 14 | path('request/', get_request), 15 | path('notice/', NoticeView.as_view()), 16 | path('navigation/', NavView.as_view()), 17 | ] 18 | -------------------------------------------------------------------------------- /spug_api/apps/host/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/host/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from apps.host.views import * 7 | from apps.host.group import GroupView 8 | from apps.host.extend import ExtendView 9 | from apps.host.add import get_regions, cloud_import 10 | 11 | urlpatterns = [ 12 | path('', HostView.as_view()), 13 | path('extend/', ExtendView.as_view()), 14 | path('group/', GroupView.as_view()), 15 | path('import/', post_import), 16 | path('import/cloud/', cloud_import), 17 | path('import/region/', get_regions), 18 | path('parse/', post_parse), 19 | path('valid/', batch_valid), 20 | ] 21 | -------------------------------------------------------------------------------- /spug_api/apps/monitor/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/monitor/management/commands/runmonitor.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.core.management.base import BaseCommand 5 | from apps.monitor.scheduler import Scheduler 6 | import logging 7 | 8 | logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(message)s') 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Start monitor process' 13 | 14 | def handle(self, *args, **options): 15 | s = Scheduler() 16 | s.run() 17 | -------------------------------------------------------------------------------- /spug_api/apps/monitor/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import models 5 | from libs import ModelMixin, human_datetime 6 | from apps.account.models import User 7 | import json 8 | 9 | 10 | class Detection(models.Model, ModelMixin): 11 | TYPES = ( 12 | ('1', '站点检测'), 13 | ('2', '端口检测'), 14 | ('3', '进程检测'), 15 | ('4', '自定义脚本'), 16 | ('5', 'Ping检测'), 17 | ) 18 | STATUS = ( 19 | (0, '正常'), 20 | (1, '异常'), 21 | ) 22 | name = models.CharField(max_length=50) 23 | type = models.CharField(max_length=2, choices=TYPES) 24 | group = models.CharField(max_length=255, null=True) 25 | targets = models.TextField() 26 | extra = models.TextField(null=True) 27 | desc = models.CharField(max_length=255, null=True) 28 | is_active = models.BooleanField(default=True) 29 | rate = models.IntegerField(default=5) 30 | threshold = models.IntegerField(default=3) 31 | quiet = models.IntegerField(default=24 * 60) 32 | fault_times = models.SmallIntegerField(default=0) 33 | notify_mode = models.CharField(max_length=255) 34 | notify_grp = models.CharField(max_length=255) 35 | latest_run_time = models.CharField(max_length=20, null=True) 36 | 37 | created_at = models.CharField(max_length=20, default=human_datetime) 38 | created_by = models.ForeignKey(User, models.PROTECT, related_name='+') 39 | updated_at = models.CharField(max_length=20, null=True) 40 | updated_by = models.ForeignKey(User, models.PROTECT, related_name='+', null=True) 41 | 42 | def to_view(self): 43 | tmp = self.to_dict() 44 | tmp['type_alias'] = self.get_type_display() 45 | tmp['notify_mode'] = json.loads(self.notify_mode) 46 | tmp['notify_grp'] = json.loads(self.notify_grp) 47 | tmp['targets'] = json.loads(self.targets) 48 | return tmp 49 | 50 | def __repr__(self): 51 | return '' % self.name 52 | 53 | class Meta: 54 | db_table = 'detections' 55 | ordering = ('-id',) 56 | -------------------------------------------------------------------------------- /spug_api/apps/monitor/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', DetectionView.as_view()), 10 | path('overview/', get_overview), 11 | path('test/', run_test), 12 | ] 13 | -------------------------------------------------------------------------------- /spug_api/apps/monitor/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import close_old_connections 5 | from apps.alarm.models import Alarm 6 | from apps.monitor.models import Detection 7 | from libs.spug import Notification 8 | import json 9 | 10 | 11 | def seconds_to_human(seconds): 12 | text = '' 13 | if seconds > 3600: 14 | text = f'{int(seconds / 3600)}小时' 15 | seconds = seconds % 3600 16 | if seconds > 60: 17 | text += f'{int(seconds / 60)}分钟' 18 | seconds = seconds % 60 19 | if seconds: 20 | text += f'{seconds}秒' 21 | return text 22 | 23 | 24 | def _record_alarm(det, target, duration, status): 25 | Alarm.objects.create( 26 | name=det.name, 27 | type=det.get_type_display(), 28 | target=target, 29 | status=status, 30 | duration=duration, 31 | notify_grp=det.notify_grp, 32 | notify_mode=det.notify_mode) 33 | 34 | 35 | def handle_notify(task_id, target, is_ok, out, fault_times): 36 | close_old_connections() 37 | det = Detection.objects.get(pk=task_id) 38 | duration = seconds_to_human(det.rate * fault_times * 60) 39 | event = '2' if is_ok else '1' 40 | _record_alarm(det, target, duration, event) 41 | grp = json.loads(det.notify_grp) 42 | notify = Notification(grp, event, target, det.name, out, duration) 43 | notify.dispatch_monitor(json.loads(det.notify_mode)) 44 | -------------------------------------------------------------------------------- /spug_api/apps/notify/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/notify/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import models 5 | from django.core.cache import cache 6 | from libs import ModelMixin, human_datetime 7 | from libs.channel import Channel 8 | import hashlib 9 | 10 | 11 | class Notify(models.Model, ModelMixin): 12 | TYPES = ( 13 | ('1', '通知'), 14 | ('2', '待办'), 15 | ) 16 | SOURCES = ( 17 | ('monitor', '监控中心'), 18 | ('schedule', '任务计划'), 19 | ('flag', '应用发布'), 20 | ('alert', '系统警告'), 21 | ) 22 | title = models.CharField(max_length=255) 23 | source = models.CharField(max_length=10, choices=SOURCES) 24 | type = models.CharField(max_length=2, choices=TYPES) 25 | content = models.TextField(null=True) 26 | unread = models.BooleanField(default=True) 27 | link = models.CharField(max_length=255, null=True) 28 | 29 | created_at = models.CharField(max_length=20, default=human_datetime) 30 | 31 | @classmethod 32 | def make_system_notify(cls, title, content): 33 | cls._make_notify('alert', '1', title, content) 34 | 35 | @classmethod 36 | def make_monitor_notify(cls, title, content): 37 | cls._make_notify('monitor', '1', title, content) 38 | 39 | @classmethod 40 | def make_schedule_notify(cls, title, content): 41 | cls._make_notify('schedule', '1', title, content) 42 | 43 | @classmethod 44 | def make_deploy_notify(cls, title, content): 45 | cls._make_notify('flag', '1', title, content) 46 | 47 | @classmethod 48 | def _make_notify(cls, source, type, title, content): 49 | tmp_str = f'{source},{type},{title},{content}' 50 | digest = hashlib.md5(tmp_str.encode()).hexdigest() 51 | unique_key = f'spug:notify:{digest}' 52 | if not cache.get(unique_key): # 限制相同内容的发送频率 53 | cache.set(unique_key, 1, 3600) 54 | cls.objects.create(source=source, title=title, type=type, content=content) 55 | Channel.send_notify(title, content) 56 | 57 | def __repr__(self): 58 | return '' % self.title 59 | 60 | class Meta: 61 | db_table = 'notifies' 62 | ordering = ('-id',) 63 | -------------------------------------------------------------------------------- /spug_api/apps/notify/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', NotifyView.as_view()), 10 | ] 11 | -------------------------------------------------------------------------------- /spug_api/apps/notify/views.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.views.generic import View 5 | from apps.notify.models import Notify 6 | from libs import json_response, JsonParser, Argument 7 | 8 | 9 | class NotifyView(View): 10 | def get(self, request): 11 | notifies = Notify.objects.all() 12 | return json_response(notifies) 13 | 14 | def patch(self, request): 15 | form, error = JsonParser( 16 | Argument('ids', type=list, help='参数错误') 17 | ).parse(request.body) 18 | if error is None: 19 | Notify.objects.filter(id__in=form.ids).update(unread=False) 20 | return json_response(error=error) 21 | -------------------------------------------------------------------------------- /spug_api/apps/repository/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/repository/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import models 5 | from django.conf import settings 6 | from libs.mixins import ModelMixin 7 | from apps.app.models import App, Environment, Deploy 8 | from apps.account.models import User 9 | from datetime import datetime 10 | import json 11 | import os 12 | 13 | 14 | class Repository(models.Model, ModelMixin): 15 | STATUS = ( 16 | ('0', '未开始'), 17 | ('1', '构建中'), 18 | ('2', '失败'), 19 | ('5', '成功'), 20 | ) 21 | app = models.ForeignKey(App, on_delete=models.PROTECT) 22 | env = models.ForeignKey(Environment, on_delete=models.PROTECT) 23 | deploy = models.ForeignKey(Deploy, on_delete=models.PROTECT) 24 | version = models.CharField(max_length=100) 25 | spug_version = models.CharField(max_length=50) 26 | remarks = models.CharField(max_length=255, null=True) 27 | extra = models.TextField() 28 | status = models.CharField(max_length=2, choices=STATUS, default='0') 29 | created_at = models.DateTimeField(auto_now_add=True) 30 | created_by = models.ForeignKey(User, on_delete=models.PROTECT) 31 | 32 | @staticmethod 33 | def make_spug_version(deploy_id): 34 | return f'{deploy_id}_{datetime.now().strftime("%Y%m%d%H%M%S")}' 35 | 36 | def to_view(self): 37 | tmp = self.to_dict() 38 | tmp['extra'] = json.loads(self.extra) 39 | tmp['status_alias'] = self.get_status_display() 40 | if hasattr(self, 'app_name'): 41 | tmp['app_name'] = self.app_name 42 | if hasattr(self, 'env_name'): 43 | tmp['env_name'] = self.env_name 44 | if hasattr(self, 'created_by_user'): 45 | tmp['created_by_user'] = self.created_by_user 46 | return tmp 47 | 48 | def delete(self, using=None, keep_parents=False): 49 | super().delete(using, keep_parents) 50 | try: 51 | build_file = f'{self.spug_version}.tar.gz' 52 | os.remove(os.path.join(settings.BUILD_DIR, build_file)) 53 | except FileNotFoundError: 54 | pass 55 | 56 | class Meta: 57 | db_table = 'repositories' 58 | ordering = ('-id',) 59 | -------------------------------------------------------------------------------- /spug_api/apps/repository/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', RepositoryView.as_view()), 10 | path('/', get_detail), 11 | path('request/', get_requests), 12 | ] 13 | -------------------------------------------------------------------------------- /spug_api/apps/schedule/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/schedule/management/commands/runscheduler.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.core.management.base import BaseCommand 5 | from apps.schedule.scheduler import Scheduler 6 | import logging 7 | 8 | logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(message)s') 9 | 10 | 11 | class Command(BaseCommand): 12 | help = 'Start schedule process' 13 | 14 | def handle(self, *args, **options): 15 | s = Scheduler() 16 | s.run() 17 | -------------------------------------------------------------------------------- /spug_api/apps/schedule/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | 6 | from .views import * 7 | 8 | urlpatterns = [ 9 | path('', Schedule.as_view()), 10 | path('/', HistoryView.as_view()), 11 | path('run_time/', next_run_time), 12 | ] 13 | -------------------------------------------------------------------------------- /spug_api/apps/setting/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/apps/setting/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import models 5 | from apps.account.models import User 6 | from libs import ModelMixin 7 | import json 8 | 9 | KEYS_DEFAULT = { 10 | 'MFA': {'enable': False}, 11 | 'verify_ip': True, 12 | 'bind_ip': True, 13 | 'ldap_service': {}, 14 | 'spug_key': None, 15 | 'api_key': None, 16 | 'mail_service': {}, 17 | 'private_key': None, 18 | 'public_key': None, 19 | 'spug_push_key': None, 20 | } 21 | 22 | 23 | class Setting(models.Model, ModelMixin): 24 | key = models.CharField(max_length=50, unique=True) 25 | value = models.TextField() 26 | desc = models.CharField(max_length=255, null=True) 27 | 28 | def to_view(self): 29 | tmp = self.to_dict(selects=('key',)) 30 | tmp['value'] = self.real_val 31 | return tmp 32 | 33 | @property 34 | def real_val(self): 35 | if self.value: 36 | return json.loads(self.value) 37 | else: 38 | return KEYS_DEFAULT.get(self.key) 39 | 40 | def __repr__(self): 41 | return '' % self.key 42 | 43 | class Meta: 44 | db_table = 'settings' 45 | 46 | 47 | class UserSetting(models.Model, ModelMixin): 48 | user = models.ForeignKey(User, on_delete=models.CASCADE) 49 | key = models.CharField(max_length=32) 50 | value = models.TextField() 51 | 52 | class Meta: 53 | db_table = 'user_settings' 54 | unique_together = ('user', 'key') 55 | -------------------------------------------------------------------------------- /spug_api/apps/setting/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | # from django.urls import path 5 | from django.conf.urls import url 6 | from apps.setting.views import * 7 | from apps.setting.user import UserSettingView 8 | 9 | urlpatterns = [ 10 | url(r'^$', SettingView.as_view()), 11 | url(r'^user/$', UserSettingView.as_view()), 12 | url(r'^ldap_test/$', ldap_test), 13 | url(r'^email_test/$', email_test), 14 | url(r'^mfa/$', MFAView.as_view()), 15 | url(r'^about/$', get_about), 16 | url(r'^push/bind/$', handle_push_bind), 17 | url(r'^push/balance/$', handle_push_balance), 18 | ] 19 | -------------------------------------------------------------------------------- /spug_api/apps/setting/user.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.views.generic import View 5 | from libs import JsonParser, Argument, json_response 6 | from apps.setting.models import UserSetting 7 | 8 | 9 | class UserSettingView(View): 10 | def get(self, request): 11 | response = {} 12 | for item in UserSetting.objects.filter(user=request.user): 13 | response[item.key] = item.value 14 | return json_response(response) 15 | 16 | def post(self, request): 17 | form, error = JsonParser( 18 | Argument('key', help='参数错误'), 19 | Argument('value', help='参数错误'), 20 | ).parse(request.body) 21 | if error is None: 22 | UserSetting.objects.update_or_create( 23 | user=request.user, 24 | key=form.key, 25 | defaults={'value': form.value} 26 | ) 27 | return self.get(request) 28 | return json_response(error=error) 29 | -------------------------------------------------------------------------------- /spug_api/apps/setting/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from functools import lru_cache 5 | from apps.setting.models import Setting, KEYS_DEFAULT 6 | from libs.ssh import SSH 7 | import json 8 | 9 | 10 | class AppSetting: 11 | @classmethod 12 | @lru_cache(maxsize=64) 13 | def get(cls, key): 14 | info = Setting.objects.filter(key=key).first() 15 | if not info: 16 | raise KeyError(f'no such key for {key!r}') 17 | return info.real_val 18 | 19 | @classmethod 20 | def get_default(cls, key, default=None): 21 | info = Setting.objects.filter(key=key).first() 22 | if not info: 23 | return default 24 | return info.real_val 25 | 26 | @classmethod 27 | def set(cls, key, value, desc=None): 28 | if key in KEYS_DEFAULT: 29 | value = json.dumps(value) 30 | Setting.objects.update_or_create(key=key, defaults={'value': value, 'desc': desc}) 31 | else: 32 | raise KeyError('invalid key') 33 | 34 | @classmethod 35 | def delete(cls, key): 36 | Setting.objects.filter(key=key).delete() 37 | 38 | @classmethod 39 | def get_ssh_key(cls): 40 | public_key = cls.get_default('public_key') 41 | private_key = cls.get_default('private_key') 42 | if not private_key or not public_key: 43 | private_key, public_key = SSH.generate_key() 44 | cls.set('private_key', private_key) 45 | cls.set('public_key', public_key) 46 | return private_key, public_key 47 | -------------------------------------------------------------------------------- /spug_api/consumer/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/consumer/routing.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.urls import path 5 | from channels.routing import URLRouter 6 | from consumer.consumers import * 7 | 8 | ws_router = URLRouter([ 9 | path('ws/ssh//', SSHConsumer), 10 | path('ws/subscribe//', PubSubConsumer), 11 | path('ws///', ComConsumer), 12 | path('ws/notify/', NotifyConsumer), 13 | ]) 14 | -------------------------------------------------------------------------------- /spug_api/consumer/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.db import close_old_connections 5 | from channels.generic.websocket import WebsocketConsumer 6 | from apps.account.models import User 7 | from apps.setting.utils import AppSetting 8 | from libs.utils import get_request_real_ip 9 | from urllib.parse import parse_qs 10 | import time 11 | 12 | 13 | def get_real_ip(headers): 14 | decode_headers = {k.decode(): v.decode() for k, v in headers} 15 | return get_request_real_ip(decode_headers) 16 | 17 | 18 | class BaseConsumer(WebsocketConsumer): 19 | def __init__(self, *args, **kwargs): 20 | super(BaseConsumer, self).__init__(*args, **kwargs) 21 | self.user = None 22 | 23 | def close_with_message(self, content): 24 | self.send(text_data=f'\r\n\x1b[31m{content}\x1b[0m\r\n') 25 | self.close() 26 | 27 | def connect(self): 28 | self.accept() 29 | close_old_connections() 30 | query_string = self.scope['query_string'].decode() 31 | x_real_ip = get_real_ip(self.scope['headers']) 32 | token = parse_qs(query_string).get('x-token', [''])[0] 33 | if token and len(token) == 32: 34 | user = User.objects.filter(access_token=token).first() 35 | if user and user.token_expired >= time.time() and user.is_active: 36 | if x_real_ip == user.last_ip or AppSetting.get_default('bind_ip') is False: 37 | self.user = user 38 | if hasattr(self, 'init'): 39 | self.init() 40 | return None 41 | self.close_with_message('触发登录IP绑定安全策略,请在系统设置/安全设置中查看配置。') 42 | self.close_with_message('用户身份验证失败,请重新登录或刷新页面。') 43 | -------------------------------------------------------------------------------- /spug_api/libs/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from .parser import JsonParser, Argument 5 | from .decorators import * 6 | from .validators import * 7 | from .mixins import * 8 | from .utils import * 9 | -------------------------------------------------------------------------------- /spug_api/libs/channel.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from channels.layers import get_channel_layer 5 | from asgiref.sync import async_to_sync 6 | import uuid 7 | 8 | layer = get_channel_layer() 9 | 10 | 11 | class Channel: 12 | @staticmethod 13 | def get_token(): 14 | return uuid.uuid4().hex 15 | 16 | @staticmethod 17 | def send_notify(title, content): 18 | message = { 19 | 'type': 'notify.message', 20 | 'title': title, 21 | 'content': content 22 | } 23 | async_to_sync(layer.group_send)('notify', message) 24 | -------------------------------------------------------------------------------- /spug_api/libs/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from functools import wraps 5 | from .utils import json_response 6 | 7 | 8 | def auth(perm_list): 9 | def decorate(view_func): 10 | codes = perm_list.split('|') 11 | 12 | @wraps(view_func) 13 | def wrapper(*args, **kwargs): 14 | user = None 15 | for item in args[:2]: 16 | if hasattr(item, 'user'): 17 | user = item.user 18 | break 19 | if user and user.has_perms(codes): 20 | return view_func(*args, **kwargs) 21 | return json_response(error='权限拒绝') 22 | 23 | return wrapper 24 | 25 | return decorate 26 | -------------------------------------------------------------------------------- /spug_api/libs/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from urllib.parse import quote, urlencode 5 | from datetime import datetime 6 | from pytz import timezone 7 | import requests 8 | import hashlib 9 | import base64 10 | import random 11 | import time 12 | import hmac 13 | import uuid 14 | 15 | 16 | def _special_url_encode(value) -> str: 17 | if isinstance(value, (str, bytes)): 18 | rst = quote(value) 19 | else: 20 | rst = urlencode(value) 21 | return rst.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') 22 | 23 | 24 | def _make_ali_signature(key: str, params: dict) -> bytes: 25 | sorted_str = _special_url_encode(dict(sorted(params.items()))) 26 | sign_str = 'GET&%2F&' + _special_url_encode(sorted_str) 27 | sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest() 28 | return base64.encodebytes(sign_digest).strip() 29 | 30 | 31 | def _make_tencent_signature(endpoint: str, key: str, params: dict) -> bytes: 32 | sorted_str = '&'.join(f'{k}={v}' for k, v in sorted(params.items())) 33 | sign_str = f'POST{endpoint}/?{sorted_str}' 34 | sign_digest = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).digest() 35 | return base64.encodebytes(sign_digest).strip() 36 | 37 | 38 | def make_ali_request(ak, ac, endpoint, params): 39 | params.update( 40 | AccessKeyId=ak, 41 | Format='JSON', 42 | SignatureMethod='HMAC-SHA1', 43 | SignatureNonce=uuid.uuid4().hex, 44 | SignatureVersion='1.0', 45 | Timestamp=datetime.now(tz=timezone('UTC')).strftime('%Y-%m-%dT%H:%M:%SZ'), 46 | Version='2014-05-26' 47 | ) 48 | params['Signature'] = _make_ali_signature(ac + '&', params) 49 | return requests.get(endpoint, params).json() 50 | 51 | 52 | def make_tencent_request(ak, ac, endpoint, params): 53 | params.update( 54 | Nonce=int(random.random() * 10000), 55 | SecretId=ak, 56 | Timestamp=int(time.time()), 57 | Version='2017-03-12' 58 | ) 59 | params['Signature'] = _make_tencent_signature(endpoint, ac, params) 60 | return requests.post(f'https://{endpoint}', data=params).json() 61 | -------------------------------------------------------------------------------- /spug_api/libs/ldap.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | import ldap 5 | 6 | 7 | class LDAP: 8 | def __init__(self, server, port, rules, admin_dn, password, base_dn): 9 | self.server = server 10 | self.port = port 11 | self.rules = rules 12 | self.admin_dn = admin_dn 13 | self.password = password 14 | self.base_dn = base_dn 15 | 16 | def valid_user(self, username, password): 17 | try: 18 | conn = ldap.initialize("ldap://{0}:{1}".format(self.server, self.port), bytes_mode=False) 19 | conn.simple_bind_s(self.admin_dn, self.password) 20 | search_filter = f'({self.rules}={username})' 21 | ldap_result_id = conn.search(self.base_dn, ldap.SCOPE_SUBTREE, search_filter, None) 22 | result_type, result_data = conn.result(ldap_result_id, 0) 23 | if result_type == ldap.RES_SEARCH_ENTRY: 24 | conn.simple_bind_s(result_data[0][0], password) 25 | return True, None 26 | else: 27 | return False, None 28 | except Exception as error: 29 | args = error.args 30 | return False, args[0].get('desc', '未知错误') if args else '%s' % error 31 | -------------------------------------------------------------------------------- /spug_api/libs/mail.py: -------------------------------------------------------------------------------- 1 | from email.header import Header 2 | from email.mime.text import MIMEText 3 | from email.utils import formataddr 4 | import smtplib 5 | 6 | 7 | class Mail: 8 | def __init__(self, server, port, username, password, nickname=None): 9 | self.host = server 10 | self.port = int(port) 11 | self.user = username 12 | self.password = password 13 | self.nickname = nickname 14 | 15 | def get_server(self): 16 | if self.port == 465: 17 | server = smtplib.SMTP_SSL(self.host, self.port) 18 | elif self.port == 587: 19 | server = smtplib.SMTP(self.host, self.port) 20 | server.ehlo() 21 | server.starttls() 22 | else: 23 | server = smtplib.SMTP(self.host, self.port) 24 | server.login(self.user, self.password) 25 | return server 26 | 27 | def send_text_mail(self, receivers, subject, body): 28 | server = self.get_server() 29 | msg = MIMEText(body, 'plain', 'utf-8') 30 | msg['Subject'] = Header(subject, 'utf-8') 31 | msg['From'] = formataddr((self.nickname, self.user)) if self.nickname else self.user 32 | server.sendmail(self.user, receivers, msg.as_string()) 33 | server.quit() 34 | -------------------------------------------------------------------------------- /spug_api/libs/middleware.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.utils.deprecation import MiddlewareMixin 5 | from django.conf import settings 6 | from .utils import json_response, get_request_real_ip 7 | from apps.account.models import User 8 | from apps.setting.utils import AppSetting 9 | import traceback 10 | import time 11 | 12 | 13 | class HandleExceptionMiddleware(MiddlewareMixin): 14 | """ 15 | 处理试图函数异常 16 | """ 17 | 18 | def process_exception(self, request, exception): 19 | traceback.print_exc() 20 | return json_response(error='Exception: %s' % exception) 21 | 22 | 23 | class AuthenticationMiddleware(MiddlewareMixin): 24 | """ 25 | 登录验证 26 | """ 27 | 28 | def process_request(self, request): 29 | if request.path in settings.AUTHENTICATION_EXCLUDES: 30 | return None 31 | if any(x.match(request.path) for x in settings.AUTHENTICATION_EXCLUDES if hasattr(x, 'match')): 32 | return None 33 | access_token = request.headers.get('x-token') or request.GET.get('x-token') 34 | if access_token and len(access_token) == 32: 35 | x_real_ip = get_request_real_ip(request.headers) 36 | user = User.objects.filter(access_token=access_token).first() 37 | if user and user.token_expired >= time.time() and user.is_active: 38 | if x_real_ip == user.last_ip or AppSetting.get_default('bind_ip') is False: 39 | request.user = user 40 | user.token_expired = time.time() + settings.TOKEN_TTL 41 | user.save() 42 | return None 43 | response = json_response(error="验证失败,请重新登录") 44 | response.status_code = 401 45 | return response 46 | -------------------------------------------------------------------------------- /spug_api/libs/mixins.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from django.views.generic import View 5 | from .utils import json_response 6 | 7 | 8 | # 混入类,提供Model实例to_dict方法 9 | class ModelMixin(object): 10 | __slots__ = () 11 | 12 | def to_dict(self, excludes: tuple = None, selects: tuple = None) -> dict: 13 | if not hasattr(self, '_meta'): 14 | raise TypeError('<%r> does not a django.db.models.Model object.' % self) 15 | elif selects: 16 | return {f: getattr(self, f) for f in selects} 17 | elif excludes: 18 | return {f.attname: getattr(self, f.attname) for f in self._meta.fields if f.attname not in excludes} 19 | else: 20 | return {f.attname: getattr(self, f.attname) for f in self._meta.fields} 21 | 22 | def update_by_dict(self, data): 23 | for key, value in data.items(): 24 | setattr(self, key, value) 25 | self.save() 26 | 27 | 28 | class AdminView(View): 29 | def dispatch(self, request, *args, **kwargs): 30 | if hasattr(request, 'user') and request.user.is_supper: 31 | return super().dispatch(request, *args, **kwargs) 32 | else: 33 | return json_response(error='权限拒绝') 34 | -------------------------------------------------------------------------------- /spug_api/libs/push.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from apps.setting.utils import AppSetting 5 | import requests 6 | 7 | push_server = 'https://push.spug.cc' 8 | 9 | 10 | def get_balance(token): 11 | res = requests.get(f'{push_server}/spug/balance/', json={'token': token}) 12 | if res.status_code != 200: 13 | raise Exception(f'status code: {res.status_code}') 14 | res = res.json() 15 | if res.get('error'): 16 | raise Exception(res['error']) 17 | return res['data'] 18 | 19 | 20 | def get_contacts(token): 21 | try: 22 | res = requests.post(f'{push_server}/spug/contacts/', json={'token': token}) 23 | res = res.json() 24 | if res['data']: 25 | return res['data'] 26 | except Exception: 27 | return [] 28 | 29 | 30 | def send_login_code(token, user, code): 31 | url = f'{push_server}/spug/message/' 32 | data = { 33 | 'token': token, 34 | 'targets': [user], 35 | 'source': 'mfa', 36 | 'dataset': { 37 | 'code': code 38 | } 39 | } 40 | res = requests.post(url, json=data, timeout=15) 41 | if res.status_code != 200: 42 | raise Exception(f'status code: {res.status_code}') 43 | res = res.json() 44 | if res.get('error'): 45 | raise Exception(res['error']) 46 | -------------------------------------------------------------------------------- /spug_api/libs/validators.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | import ipaddress 5 | from datetime import datetime 6 | 7 | 8 | # 判断是否是ip地址 9 | def ip_validator(value): 10 | try: 11 | ipaddress.ip_address(value) 12 | return True 13 | except ValueError: 14 | return False 15 | 16 | 17 | # 判断是否是日期字符串,支持 2018-04-11 或 2018-04-11 14:55:30 18 | def date_validator(value: str) -> bool: 19 | value = value.strip() 20 | try: 21 | if len(value) == 10: 22 | datetime.strptime(value, '%Y-%m-%d') 23 | return True 24 | elif len(value) == 19: 25 | datetime.strptime(value, '%Y-%m-%d %H:%M:%S') 26 | return True 27 | except ValueError: 28 | pass 29 | return False 30 | -------------------------------------------------------------------------------- /spug_api/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_api/logs/.gitkeep -------------------------------------------------------------------------------- /spug_api/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | """Django's command-line utility for administrative tasks.""" 6 | import os 7 | import sys 8 | 9 | 10 | def main(): 11 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spug.settings') 12 | try: 13 | from django.core.management import execute_from_command_line 14 | except ImportError as exc: 15 | raise ImportError( 16 | "Couldn't import Django. Are you sure it's installed and " 17 | "available on your PYTHONPATH environment variable? Did you " 18 | "forget to activate a virtual environment?" 19 | ) from exc 20 | execute_from_command_line(sys.argv) 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /spug_api/repos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_api/repos/.gitkeep -------------------------------------------------------------------------------- /spug_api/repos/build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_api/repos/build/.gitkeep -------------------------------------------------------------------------------- /spug_api/requirements.txt: -------------------------------------------------------------------------------- 1 | apscheduler==3.7.0 2 | Django==2.2.28 3 | asgiref==3.2.10 4 | channels==2.3.1 5 | channels_redis==2.4.1 6 | paramiko==2.11.0 7 | django-redis==4.10.0 8 | requests==2.32.0 9 | GitPython==3.1.41 10 | python-ldap==3.4.0 11 | openpyxl==3.0.3 12 | user_agents==2.2.0 -------------------------------------------------------------------------------- /spug_api/spug/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | -------------------------------------------------------------------------------- /spug_api/spug/asgi.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | """ 5 | ASGI entrypoint. Configures Django and then runs the application 6 | defined in the ASGI_APPLICATION setting. 7 | """ 8 | 9 | 10 | import os 11 | import django 12 | from channels.routing import get_default_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spug.settings') 15 | django.setup() 16 | application = get_default_application() 17 | -------------------------------------------------------------------------------- /spug_api/spug/routing.py: -------------------------------------------------------------------------------- 1 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 2 | # Copyright: (c) 3 | # Released under the AGPL-3.0 License. 4 | from channels.routing import ProtocolTypeRouter 5 | from consumer import routing 6 | 7 | application = ProtocolTypeRouter({ 8 | 'websocket': routing.ws_router 9 | }) 10 | -------------------------------------------------------------------------------- /spug_api/spug/urls.py: -------------------------------------------------------------------------------- 1 | """spug URL Configuration 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | 6 | The `urlpatterns` list routes URLs to views. For more information please see: 7 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 8 | Examples: 9 | Function views 10 | 1. Add an import: from my_app import views 11 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 12 | Class-based views 13 | 1. Add an import: from other_app.views import Home 14 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 15 | Including another URLconf 16 | 1. Import the include() function: from django.urls import include, path 17 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 18 | """ 19 | from django.urls import path, include 20 | 21 | urlpatterns = [ 22 | path('account/', include('apps.account.urls')), 23 | path('host/', include('apps.host.urls')), 24 | path('exec/', include('apps.exec.urls')), 25 | path('schedule/', include('apps.schedule.urls')), 26 | path('monitor/', include('apps.monitor.urls')), 27 | path('alarm/', include('apps.alarm.urls')), 28 | path('setting/', include('apps.setting.urls')), 29 | path('config/', include('apps.config.urls')), 30 | path('app/', include('apps.app.urls')), 31 | path('deploy/', include('apps.deploy.urls')), 32 | path('repository/', include('apps.repository.urls')), 33 | path('home/', include('apps.home.urls')), 34 | path('notify/', include('apps.notify.urls')), 35 | path('file/', include('apps.file.urls')), 36 | path('apis/', include('apps.apis.urls')), 37 | ] 38 | -------------------------------------------------------------------------------- /spug_api/spug/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | WSGI config for spug project. 6 | 7 | It exposes the WSGI callable as a module-level variable named ``application``. 8 | 9 | For more information on this file, see 10 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 11 | """ 12 | 13 | import os 14 | 15 | from django.core.wsgi import get_wsgi_application 16 | 17 | 18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spug.settings') 19 | 20 | application = get_wsgi_application() 21 | -------------------------------------------------------------------------------- /spug_api/storage/transfer/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_api/storage/transfer/.gitkeep -------------------------------------------------------------------------------- /spug_api/tools/migrate.py: -------------------------------------------------------------------------------- 1 | import django 2 | import sys 3 | import os 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.append(BASE_DIR) 7 | 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spug.settings") 9 | django.setup() 10 | 11 | from django.conf import settings 12 | import subprocess 13 | import shutil 14 | import sys 15 | import os 16 | import re 17 | 18 | 19 | class Version: 20 | def __init__(self, version): 21 | self.version = re.sub('[^0-9.]', '', version).split('.') 22 | 23 | def __gt__(self, other): 24 | if not isinstance(other, Version): 25 | raise TypeError('required type Version') 26 | for v1, v2 in zip(self.version, other.version): 27 | if int(v1) == int(v2): 28 | continue 29 | elif int(v1) > int(v2): 30 | return True 31 | else: 32 | return False 33 | return False 34 | 35 | 36 | if __name__ == '__main__': 37 | old_version = Version(sys.argv[1]) 38 | now_version = Version(settings.SPUG_VERSION) 39 | if old_version < Version('v3.0.2'): 40 | old_path = os.path.join(settings.BASE_DIR, 'repos') 41 | new_path = os.path.join(settings.REPOS_DIR) 42 | if not os.path.exists(new_path): 43 | print('执行 v3.0.1-beta.8 repos目录迁移') 44 | shutil.move(old_path, new_path) 45 | task = subprocess.Popen(f'cd {settings.BASE_DIR} && git checkout -- repos', shell=True) 46 | if task.wait() != 0: 47 | print('repos目录迁移失败,请联系官方人员') 48 | -------------------------------------------------------------------------------- /spug_api/tools/start-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | # start api service 6 | 7 | cd $(dirname $(dirname $0)) 8 | if [ -f ./venv/bin/activate ]; then 9 | source ./venv/bin/activate 10 | fi 11 | exec gunicorn -b 127.0.0.1:9001 -w 2 --threads 8 --access-logfile - spug.wsgi 12 | -------------------------------------------------------------------------------- /spug_api/tools/start-monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | # start monitor service 6 | 7 | cd $(dirname $(dirname $0)) 8 | if [ -f ./venv/bin/activate ]; then 9 | source ./venv/bin/activate 10 | fi 11 | 12 | if command -v python3 &> /dev/null; then 13 | PYTHON=python3 14 | else 15 | PYTHON=python 16 | fi 17 | 18 | exec $PYTHON manage.py runmonitor 19 | -------------------------------------------------------------------------------- /spug_api/tools/start-scheduler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | # start schedule service 6 | 7 | cd $(dirname $(dirname $0)) 8 | if [ -f ./venv/bin/activate ]; then 9 | source ./venv/bin/activate 10 | fi 11 | 12 | if command -v python3 &> /dev/null; then 13 | PYTHON=python3 14 | else 15 | PYTHON=python 16 | fi 17 | 18 | exec $PYTHON manage.py runscheduler 19 | -------------------------------------------------------------------------------- /spug_api/tools/start-worker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | # start worker service 6 | 7 | cd $(dirname $(dirname $0)) 8 | if [ -f ./venv/bin/activate ]; then 9 | source ./venv/bin/activate 10 | fi 11 | 12 | if command -v python3 &> /dev/null; then 13 | PYTHON=python3 14 | else 15 | PYTHON=python 16 | fi 17 | 18 | exec $PYTHON manage.py runworker 19 | -------------------------------------------------------------------------------- /spug_api/tools/start-ws.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug 3 | # Copyright: (c) 4 | # Released under the AGPL-3.0 License. 5 | # start websocket service 6 | 7 | cd $(dirname $(dirname $0)) 8 | if [ -f ./venv/bin/activate ]; then 9 | source ./venv/bin/activate 10 | fi 11 | exec daphne -p 9002 spug.asgi:application 12 | -------------------------------------------------------------------------------- /spug_api/tools/supervisor-spug.ini: -------------------------------------------------------------------------------- 1 | [program:spug-api] 2 | command = bash /data/spug/spug_api/tools/start-api.sh 3 | autostart = true 4 | stdout_logfile = /data/spug/spug_api/logs/api.log 5 | redirect_stderr = true 6 | 7 | [program:spug-ws] 8 | command = bash /data/spug/spug_api/tools/start-ws.sh 9 | autostart = true 10 | stdout_logfile = /data/spug/spug_api/logs/ws.log 11 | redirect_stderr = true 12 | 13 | [program:spug-worker] 14 | command = bash /data/spug/spug_api/tools/start-worker.sh 15 | autostart = true 16 | stdout_logfile = /data/spug/spug_api/logs/worker.log 17 | redirect_stderr = true 18 | 19 | [program:spug-monitor] 20 | command = bash /data/spug/spug_api/tools/start-monitor.sh 21 | autostart = true 22 | stdout_logfile = /data/spug/spug_api/logs/monitor.log 23 | redirect_stderr = true 24 | 25 | [program:spug-scheduler] 26 | command = bash /data/spug/spug_api/tools/start-scheduler.sh 27 | autostart = true 28 | stdout_logfile = /data/spug/spug_api/logs/scheduler.log 29 | redirect_stderr = true -------------------------------------------------------------------------------- /spug_web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /.idea/ 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /spug_web/README.md: -------------------------------------------------------------------------------- 1 | spug web -------------------------------------------------------------------------------- /spug_web/config-overrides.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | const {override, addDecoratorsLegacy, addLessLoader} = require('customize-cra'); 7 | 8 | module.exports = override( 9 | addDecoratorsLegacy(), 10 | addLessLoader({ 11 | lessOptions: { 12 | javascriptEnabled: true, 13 | modifyVars: { 14 | '@primary-color': '#2563fc' 15 | } 16 | } 17 | }), 18 | ); 19 | -------------------------------------------------------------------------------- /spug_web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": [ 6 | "src" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /spug_web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spug_web", 3 | "version": "3.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.3.0", 7 | "ace-builds": "^1.4.13", 8 | "antd": "4.21.5", 9 | "axios": "^0.21.0", 10 | "bizcharts": "^3.5.9", 11 | "history": "^4.10.1", 12 | "lodash": "^4.17.19", 13 | "mobx": "^5.15.6", 14 | "mobx-react": "^6.3.0", 15 | "moment": "^2.24.0", 16 | "react": "^16.13.1", 17 | "react-ace": "^9.5.0", 18 | "react-dom": "^16.13.1", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "3.4.3", 21 | "xterm": "^4.6.0", 22 | "xterm-addon-fit": "^0.5.0" 23 | }, 24 | "scripts": { 25 | "start": "react-app-rewired start", 26 | "build": "GENERATE_SOURCEMAP=false react-app-rewired build", 27 | "test": "react-app-rewired test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@babel/plugin-proposal-decorators": "^7.10.5", 47 | "customize-cra": "^1.0.0", 48 | "http-proxy-middleware": "0.19.2", 49 | "less": "^3.12.2", 50 | "less-loader": "^7.1.0", 51 | "react-app-rewired": "^2.1.6" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /spug_web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/favicon.ico -------------------------------------------------------------------------------- /spug_web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Spug 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /spug_web/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/logo.png -------------------------------------------------------------------------------- /spug_web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Spug", 3 | "name": "Spug", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /spug_web/public/resource/gitee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/gitee.png -------------------------------------------------------------------------------- /spug_web/public/resource/gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/gitlab.png -------------------------------------------------------------------------------- /spug_web/public/resource/grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/grafana.png -------------------------------------------------------------------------------- /spug_web/public/resource/prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/prometheus.png -------------------------------------------------------------------------------- /spug_web/public/resource/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/wiki.png -------------------------------------------------------------------------------- /spug_web/public/resource/主机导入模板.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/public/resource/主机导入模板.xlsx -------------------------------------------------------------------------------- /spug_web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /spug_web/src/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React, { Component } from 'react'; 7 | import {Switch, Route} from 'react-router-dom'; 8 | import Login from './pages/login'; 9 | import WebSSH from './pages/ssh'; 10 | import Layout from './layout'; 11 | 12 | class App extends Component { 13 | render() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /spug_web/src/components/ACEditor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import Editor from 'react-ace'; 8 | import 'ace-builds/src-noconflict/mode-sh'; 9 | import 'ace-builds/src-noconflict/mode-text'; 10 | import 'ace-builds/src-noconflict/mode-json'; 11 | import 'ace-builds/src-noconflict/mode-space'; 12 | import 'ace-builds/src-noconflict/mode-python'; 13 | import 'ace-builds/src-noconflict/theme-tomorrow'; 14 | 15 | export default function (props) { 16 | const style = {fontFamily: 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei', ...props.style} 17 | return ( 18 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /spug_web/src/components/Action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Link as ALink } from 'react-router-dom'; 8 | import { Divider, Button as AButton } from 'antd'; 9 | import { hasPermission } from 'libs'; 10 | 11 | function canVisible(auth) { 12 | return !auth || hasPermission(auth) 13 | } 14 | 15 | class Action extends React.Component { 16 | static Link(props) { 17 | return 18 | } 19 | 20 | static Button(props) { 21 | return 22 | } 23 | 24 | _handle = (data, el) => { 25 | const length = data.length; 26 | if (el && canVisible(el.props.auth)) { 27 | if (length !== 0) data.push() 28 | data.push(el) 29 | } 30 | } 31 | 32 | render() { 33 | const children = []; 34 | if (Array.isArray(this.props.children)) { 35 | this.props.children.forEach(el => this._handle(children, el)) 36 | } else { 37 | this._handle(children, this.props.children) 38 | } 39 | 40 | return 41 | {children} 42 | 43 | } 44 | } 45 | 46 | export default Action 47 | -------------------------------------------------------------------------------- /spug_web/src/components/AuthButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Button } from 'antd'; 8 | import { hasPermission } from 'libs'; 9 | 10 | 11 | export default function AuthButton(props) { 12 | let disabled = props.disabled; 13 | if (props.auth && !hasPermission(props.auth)) { 14 | disabled = true; 15 | } 16 | return disabled ? null : 17 | } 18 | -------------------------------------------------------------------------------- /spug_web/src/components/AuthCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import {Card} from 'antd'; 8 | import { hasPermission } from 'libs'; 9 | 10 | 11 | export default function AuthCard(props) { 12 | let disabled = props.disabled === undefined ? false : props.disabled; 13 | if (props.auth && !hasPermission(props.auth)) { 14 | disabled = true; 15 | } 16 | return disabled ? null : {props.children} 17 | } 18 | -------------------------------------------------------------------------------- /spug_web/src/components/AuthDiv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { hasPermission } from 'libs'; 8 | 9 | 10 | export default function AuthDiv(props) { 11 | let disabled = props.disabled === undefined ? false : props.disabled; 12 | if (props.auth && !hasPermission(props.auth)) { 13 | disabled = true; 14 | } 15 | return disabled ? null :
{props.children}
16 | } 17 | -------------------------------------------------------------------------------- /spug_web/src/components/AuthFragment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { hasPermission } from 'libs'; 8 | 9 | 10 | export default function AuthFragment(props) { 11 | return hasPermission(props.auth) ? {props.children} : null 12 | } 13 | -------------------------------------------------------------------------------- /spug_web/src/components/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Breadcrumb } from 'antd'; 8 | import styles from './index.module.less'; 9 | 10 | 11 | export default class extends React.Component { 12 | static Item = Breadcrumb.Item 13 | 14 | render() { 15 | let title = this.props.title; 16 | if (!title) { 17 | const rawChildren = this.props.children; 18 | if (Array.isArray(rawChildren)) { 19 | title = rawChildren[rawChildren.length - 1].props.children 20 | } else { 21 | title = rawChildren.props.children 22 | } 23 | } 24 | 25 | return ( 26 |
27 | 28 | {this.props.children} 29 | 30 | {this.props.extra ? ( 31 |
32 | {title} 33 | {this.props.extra} 34 |
35 | ) : null} 36 |
37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /spug_web/src/components/Link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react' 7 | 8 | 9 | function Link(props) { 10 | return ( 11 | 16 | {props.title} 17 | ) 18 | } 19 | 20 | export default Link -------------------------------------------------------------------------------- /spug_web/src/components/LinkButton.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Button } from 'antd'; 8 | import { hasPermission } from 'libs'; 9 | 10 | 11 | export default function LinkButton(props) { 12 | let disabled = props.disabled; 13 | if (props.auth && !hasPermission(props.auth)) { 14 | disabled = true; 15 | } 16 | return 19 | } 20 | -------------------------------------------------------------------------------- /spug_web/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.module.less'; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |

404

12 |
抱歉,你访问的页面不存在
13 |
14 |
15 | ) 16 | } -------------------------------------------------------------------------------- /spug_web/src/components/SearchForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Row, Col, Form } from 'antd'; 8 | import styles from './index.module.less'; 9 | 10 | export default class extends React.Component { 11 | static Item(props) { 12 | return ( 13 | 14 | 15 | {props.children} 16 | 17 | 18 | ) 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |
25 | 26 | {this.props.children} 27 | 28 |
29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spug_web/src/components/StatisticsCard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Card, Col, Row } from 'antd'; 8 | import lodash from 'lodash'; 9 | import styles from './index.module.less'; 10 | 11 | 12 | class StatisticsCard extends React.Component { 13 | static Item = (props) => { 14 | return ( 15 |
16 | {props.title} 17 |

{props.value}

18 | {props.bordered !== false && } 19 |
20 | ) 21 | }; 22 | 23 | render() { 24 | let items = lodash.get(this.props, 'children', []); 25 | if (!lodash.isArray(items)) items = [items]; 26 | const span = Math.ceil(24 / (items.length || 1)); 27 | return ( 28 | 29 | 30 | {items.map((item, index) => ( 31 | 32 | {item} 33 | 34 | ))} 35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | export default StatisticsCard 42 | -------------------------------------------------------------------------------- /spug_web/src/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import StatisticsCard from './StatisticsCard'; 7 | import SearchForm from './SearchForm'; 8 | import LinkButton from './LinkButton'; 9 | import AuthButton from './AuthButton'; 10 | import AuthFragment from './AuthFragment'; 11 | import AuthCard from './AuthCard'; 12 | import AuthDiv from './AuthDiv'; 13 | import ACEditor from './ACEditor'; 14 | import Action from './Action'; 15 | import TableCard from './TableCard'; 16 | import Breadcrumb from './Breadcrumb'; 17 | import AppSelector from './AppSelector'; 18 | import NotFound from './NotFound'; 19 | import Link from './Link'; 20 | 21 | export { 22 | StatisticsCard, 23 | AuthFragment, 24 | SearchForm, 25 | LinkButton, 26 | AuthButton, 27 | AuthCard, 28 | AuthDiv, 29 | ACEditor, 30 | Action, 31 | TableCard, 32 | Breadcrumb, 33 | AppSelector, 34 | NotFound, 35 | Link, 36 | } 37 | -------------------------------------------------------------------------------- /spug_web/src/gStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import { observable } from 'mobx'; 7 | import http from 'libs/http'; 8 | import themes from 'pages/ssh/themes'; 9 | 10 | class Store { 11 | isReady = false; 12 | @observable terminal = { 13 | fontSize: 16, 14 | fontFamily: 'Courier', 15 | theme: 'dark', 16 | styles: themes['dark'] 17 | }; 18 | 19 | _handleSettings = (res) => { 20 | if (res.terminal) { 21 | const terminal = JSON.parse(res.terminal) 22 | const styles = themes[terminal.theme] 23 | if (styles) { 24 | terminal.styles = styles 25 | } else { 26 | terminal.styles = themes['dark'] 27 | terminal.theme = 'dark' 28 | } 29 | this.terminal = terminal 30 | } 31 | } 32 | 33 | fetchUserSettings = () => { 34 | if (this.isReady) return 35 | http.get('/api/setting/user/') 36 | .then(res => { 37 | this.isReady = true 38 | this._handleSettings(res) 39 | }) 40 | }; 41 | 42 | updateUserSettings = (key, value) => { 43 | return http.post('/api/setting/user/', {key, value}) 44 | .then(res => { 45 | this.isReady = true 46 | this._handleSettings(res) 47 | }) 48 | } 49 | } 50 | 51 | export default new Store() -------------------------------------------------------------------------------- /spug_web/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { Router } from 'react-router-dom'; 9 | import { ConfigProvider } from 'antd'; 10 | import zhCN from 'antd/es/locale/zh_CN'; 11 | import './index.less'; 12 | import App from './App'; 13 | import moment from 'moment'; 14 | import 'moment/locale/zh-cn'; 15 | import * as serviceWorker from './serviceWorker'; 16 | import { history, updatePermissions } from 'libs'; 17 | 18 | moment.locale('zh-cn'); 19 | updatePermissions(); 20 | 21 | ReactDOM.render( 22 | 23 | document.fullscreenElement || document.body}> 24 | 25 | 26 | , 27 | document.getElementById('root') 28 | ); 29 | 30 | // If you want your app to work offline and load faster, you can change 31 | // unregister() to register() below. Note this comes with some pitfalls. 32 | // Learn more about service workers: https://bit.ly/CRA-PWA 33 | serviceWorker.unregister(); 34 | -------------------------------------------------------------------------------- /spug_web/src/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.less'; 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | overflow: hidden; 9 | } 10 | 11 | code { 12 | font-family: Source Code Pro, Menlo, Monaco, Consolas, Courier New, monospace, Courier, PingFang SC, Microsoft YaHei; 13 | } 14 | 15 | .ant-form-item-extra { 16 | font-size: 13px; 17 | padding-top: 6px; 18 | } 19 | 20 | /* Common CSS style */ 21 | .none { 22 | display: none; 23 | } 24 | 25 | .btn { 26 | color: #2563fc; 27 | cursor: pointer; 28 | } -------------------------------------------------------------------------------- /spug_web/src/layout/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React from 'react'; 7 | import { Layout } from 'antd'; 8 | import { CopyrightOutlined, GithubOutlined } from '@ant-design/icons'; 9 | import styles from './layout.module.less'; 10 | 11 | 12 | export default function () { 13 | return ( 14 | 15 |
16 |
17 | 官网 19 | 21 | 文档 23 |
24 |
25 | Copyright {new Date().getFullYear()} By OpenSpug 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /spug_web/src/layout/Sider.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Layout, Menu } from 'antd'; 3 | import { hasPermission, history } from 'libs'; 4 | import styles from './layout.module.less'; 5 | import routes from '../routes'; 6 | import logo from './logo-spug-white.png'; 7 | 8 | let selectedKey = window.location.pathname; 9 | const OpenKeysMap = {}; 10 | for (let item of routes) { 11 | if (item.child) { 12 | for (let sub of item.child) { 13 | if (sub.title) OpenKeysMap[sub.path] = item.title 14 | } 15 | } else if (item.title) { 16 | OpenKeysMap[item.path] = 1 17 | } 18 | } 19 | 20 | export default function Sider(props) { 21 | const [openKeys, setOpenKeys] = useState([]); 22 | const [menus, setMenus] = useState([]); 23 | 24 | useEffect(() => { 25 | const tmp = [] 26 | for (let item of routes) { 27 | const menu = handleRoute(item) 28 | tmp.push(menu) 29 | } 30 | setMenus(tmp) 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, []) 33 | 34 | function handleRoute(item) { 35 | if (item.auth && !hasPermission(item.auth)) return 36 | if (!item.title) return; 37 | const menu = {label: item.title, key: item.path, icon: item.icon} 38 | if (item.child) { 39 | menu.children = [] 40 | for (let sub of item.child) { 41 | const subMenu = handleRoute(sub) 42 | menu.children.push(subMenu) 43 | } 44 | } 45 | return menu 46 | } 47 | 48 | const tmp = window.location.pathname; 49 | const openKey = OpenKeysMap[tmp]; 50 | if (openKey) { 51 | selectedKey = tmp; 52 | if (openKey !== 1 && !props.collapsed && !openKeys.includes(openKey)) { 53 | setOpenKeys([...openKeys, openKey]) 54 | } 55 | } 56 | return ( 57 | 58 |
59 | Logo 60 |
61 |
62 | history.push(menu.key)}/> 71 |
72 |
73 | ) 74 | } -------------------------------------------------------------------------------- /spug_web/src/layout/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openspug/spug/0e7b5ec77e5f29fc418984a8f8584e8660a48b43/spug_web/src/layout/avatar.png -------------------------------------------------------------------------------- /spug_web/src/layout/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) OpenSpug Organization. https://github.com/openspug/spug 3 | * Copyright (c) 4 | * Released under the AGPL-3.0 License. 5 | */ 6 | import React, { useState, useEffect } from 'react'; 7 | import { Switch, Route } from 'react-router-dom'; 8 | import { Layout, message } from 'antd'; 9 | import { NotFound } from 'components'; 10 | import Sider from './Sider'; 11 | import Header from './Header'; 12 | import Footer from './Footer' 13 | import routes from '../routes'; 14 | import { hasPermission, isMobile } from 'libs'; 15 | import styles from './layout.module.less'; 16 | 17 | function initRoutes(Routes, routes) { 18 | for (let route of routes) { 19 | if (route.component) { 20 | if (!route.auth || hasPermission(route.auth)) { 21 | Routes.push() 22 | } 23 | } else if (route.child) { 24 | initRoutes(Routes, route.child) 25 | } 26 | } 27 | } 28 | 29 | export default function () { 30 | const [collapsed, setCollapsed] = useState(false) 31 | const [Routes, setRoutes] = useState([]); 32 | 33 | useEffect(() => { 34 | if (isMobile) { 35 | setCollapsed(true); 36 | message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5) 37 | } 38 | const Routes = []; 39 | initRoutes(Routes, routes); 40 | setRoutes(Routes) 41 | }, []) 42 | 43 | return ( 44 | 45 | 46 | 47 |
setCollapsed(!collapsed)}/> 48 | 49 | 50 | {Routes} 51 | 52 | 53 | 54 |