├── .gitignore ├── .idea ├── .gitignore ├── SmartPXE.iml ├── dataSources.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── dashboard ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── frontends ├── css │ ├── app.c916690f.css │ └── chunk-vendors.6b196170.css ├── favicon.ico ├── fonts │ ├── element-icons.535877f5.woff │ └── element-icons.732389de.ttf ├── img │ └── logo.89a4c943.png ├── index.html └── js │ ├── app.fae1315c.js │ ├── app.fae1315c.js.map │ ├── chunk-vendors.badef4b9.js │ └── chunk-vendors.badef4b9.js.map ├── install ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── requirements ├── requirements.old ├── service_conf ├── daemon.json ├── dnsmasq.conf ├── pip.conf ├── server.smartpxe.com.conf ├── smartpxe.service ├── sources.list ├── uwsgi.ini └── www.smartpxe.com.conf ├── setup.py ├── smartpxe ├── __init__.py ├── asgi.py ├── celery.py ├── settings.py ├── urls.py └── wsgi.py ├── task ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── tasks.py ├── tests.py ├── urls.py └── views.py ├── temp ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py └── utils ├── __init__.py ├── exceptions.py ├── healthy_check.py ├── paginations.py ├── permissions.py └── tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | # .idea/artifacts 33 | # .idea/compiler.xml 34 | # .idea/jarRepositories.xml 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | # *.iml 39 | # *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | ### Linux template 75 | *~ 76 | 77 | # temporary files which can be created if a process still has a handle open of a deleted file 78 | .fuse_hidden* 79 | 80 | # KDE directory preferences 81 | .directory 82 | 83 | # Linux trash folder which might appear on any partition or disk 84 | .Trash-* 85 | 86 | # .nfs files are created when an open file is removed but is still being accessed 87 | .nfs* 88 | 89 | ### macOS template 90 | # General 91 | .DS_Store 92 | .AppleDouble 93 | .LSOverride 94 | 95 | # Icon must end with two \r 96 | Icon 97 | 98 | # Thumbnails 99 | ._* 100 | 101 | # Files that might appear in the root of a volume 102 | .DocumentRevisions-V100 103 | .fseventsd 104 | .Spotlight-V100 105 | .TemporaryItems 106 | .Trashes 107 | .VolumeIcon.icns 108 | .com.apple.timemachine.donotpresent 109 | 110 | # Directories potentially created on remote AFP share 111 | .AppleDB 112 | .AppleDesktop 113 | Network Trash Folder 114 | Temporary Items 115 | .apdisk 116 | 117 | ### Python template 118 | # Byte-compiled / optimized / DLL files 119 | __pycache__/ 120 | *.py[cod] 121 | *$py.class 122 | 123 | # C extensions 124 | *.so 125 | 126 | # Distribution / packaging 127 | .Python 128 | build/ 129 | develop-eggs/ 130 | dist/ 131 | downloads/ 132 | eggs/ 133 | .eggs/ 134 | lib/ 135 | lib64/ 136 | parts/ 137 | sdist/ 138 | var/ 139 | wheels/ 140 | share/python-wheels/ 141 | *.egg-info/ 142 | .installed.cfg 143 | *.egg 144 | MANIFEST 145 | 146 | # PyInstaller 147 | # Usually these files are written by a python script from a template 148 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 149 | *.manifest 150 | *.spec 151 | 152 | # Installer logs 153 | pip-log.txt 154 | pip-delete-this-directory.txt 155 | 156 | # Unit test / coverage reports 157 | htmlcov/ 158 | .tox/ 159 | .nox/ 160 | .coverage 161 | .coverage.* 162 | .cache 163 | nosetests.xml 164 | coverage.xml 165 | *.cover 166 | *.py,cover 167 | .hypothesis/ 168 | .pytest_cache/ 169 | cover/ 170 | 171 | # Translations 172 | *.mo 173 | *.pot 174 | 175 | # Django stuff: 176 | *.log 177 | local_settings.py 178 | db.sqlite3 179 | db.sqlite3-journal 180 | 181 | # Flask stuff: 182 | instance/ 183 | .webassets-cache 184 | 185 | # Scrapy stuff: 186 | .scrapy 187 | 188 | # Sphinx documentation 189 | docs/_build/ 190 | 191 | # PyBuilder 192 | .pybuilder/ 193 | target/ 194 | 195 | # Jupyter Notebook 196 | .ipynb_checkpoints 197 | 198 | # IPython 199 | profile_default/ 200 | ipython_config.py 201 | 202 | # pyenv 203 | # For a library or package, you might want to ignore these files since the code is 204 | # intended to run in multiple environments; otherwise, check them in: 205 | # .python-version 206 | 207 | # pipenv 208 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 209 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 210 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 211 | # install all needed dependencies. 212 | #Pipfile.lock 213 | 214 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 215 | __pypackages__/ 216 | 217 | # Celery stuff 218 | celerybeat-schedule 219 | celerybeat.pid 220 | 221 | # SageMath parsed files 222 | *.sage.py 223 | 224 | # Environments 225 | .env 226 | .venv 227 | env/ 228 | venv/ 229 | ENV/ 230 | env.bak/ 231 | venv.bak/ 232 | 233 | # Spyder project settings 234 | .spyderproject 235 | .spyproject 236 | 237 | # Rope project settings 238 | .ropeproject 239 | 240 | # mkdocs documentation 241 | /site 242 | 243 | # mypy 244 | .mypy_cache/ 245 | .dmypy.json 246 | dmypy.json 247 | 248 | # Pyre type checker 249 | .pyre/ 250 | 251 | # pytype static type analyzer 252 | .pytype/ 253 | 254 | # Cython debug symbols 255 | cython_debug/ 256 | 257 | # django 258 | migrations/ 259 | 260 | t*.py -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/SmartPXE.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://10.10.100.2:3306 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目简介 2 | 3 | 可以通过该平台管理计算机设备系统安装的生命周期。 4 | 5 | ## 项目技术栈 6 | 7 | ``` 8 | 前端 9 | vue2.0 + element-ui 10 | 后端 11 | django3.2 + jwt + python3.8.8 + mysql5.7 12 | 13 | 应用到的linux服务 14 | dnsmasq(dhcp, tftp, dns) 15 | nginx 16 | 17 | 任务运行使用的技术 18 | ansilbe_runner 19 | redis 20 | celery 21 | ``` 22 | 23 | ## 项目功能 24 | 25 | ``` 26 | 该项目是为了更好的管理计算机(服务器)部署系统,使用了pxe -> ipxe网络启动方式,ipxe使用http启动速度更快。 27 | 在部署系统前: 28 | 你可以看到待安装计算机设备的硬件信息。所以,你可以根据硬件信息对计算机设备进行分类安装; 29 | 你也可以在对计算机设备部署操作系统前对其执行任务(例如:通过脚本实现ipmi配置、raid配置); 30 | 在部署系统中: 31 | 你可以看到部署的详细进度; 32 | 在部署完成后: 33 | 你可以看到部署后的结果和部署中的进度; 34 | ``` 35 | 36 | ## 项目文档与演示 37 | 38 | 项目演示 http://smartpxe_demo.linux98.com 39 | 40 | 项目文档 http://smartpxe.linux98.com 41 | 42 | 部署项目 http://smartpxe.linux98.com/document/install.html 43 | 44 | 视频演示 https://www.bilibili.com/video/BV1C34y187ph 45 | 46 | ## 项目进度 47 | 48 | 目前项目处于demo阶段,功能会陆续增加。。。 49 | 50 | - [x] 系统安装方向 51 | - [x] 镜像和模板的管理 52 | - [x] 新增 53 | - [x] 编辑(仅支持模板管理) 54 | - [x] 查看 55 | - [x] 删除 56 | - [x] 系统部署的管理 57 | - [x] 硬件信息收集 58 | - [x] 系统部署 59 | - [x] 日志收集 60 | - [x] 保留记录 61 | - [x] 任务系统方向 62 | - [x] 运行model命令 63 | - [x] 运行playbook命令 64 | - [x] 收集playbook运行结果和操作记录 65 | 66 | 下个阶段准备上线的功能: 67 | - [ ] 硬件配置 68 | - [ ] ipmi配置 69 | - [ ] 阵列卡配置 70 | 71 | 72 | ## 参考地址 73 | 74 | ``` 75 | BootOS 76 | https://www.xiaocoder.com/2020/03/29/build-bootos-system/ 77 | 78 | Cloud Boot 79 | https://github.com/idcos/osinstall 80 | 81 | archlinux 82 | wiki.archlinux.org 83 | 84 | 创建自定义 Ubuntu 映像 85 | https://maas.io/docs/snap/2.9/ui/creating-a-custom-ubuntu-image 86 | 87 | 基于物理服务器进行ramos定制 88 | http://www.360doc.com/content/20/1218/13/13328254_952190955.shtml 89 | 90 | 构建内存OS,基于ubuntu 91 | http://linuxcoming.com/blog/2019/06/21/build_ram_os.html 92 | 93 | 网络引导安装ubuntu 94 | https://ubuntu.com/server/docs/install/netboot-amd64 95 | 96 | 自定义initramfs 97 | https://wiki.gentoo.org/wiki/Custom_Initramfs 98 | 99 | 精通initramfs 100 | https://www.cnblogs.com/ztguang/p/12647255.html 101 | 102 | PXELINUX 103 | https://wiki.syslinux.org/wiki/index.php?title=PXELINUX 104 | 105 | Redhat 106 | https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/6/html/installation_guide/sn-booting-from-pxe-x86 107 | ``` 108 | 109 | 110 | ## 开发项目 111 | 112 | ### 配置pip 113 | 114 | ```bash 115 | mkdir ~/.pip 116 | vim ~/.pip/pip.conf 117 | 118 | # 输入下面的内容 119 | [global] 120 | index-url = https://mirrors.aliyun.com/pypi/simple/ 121 | 122 | [install] 123 | trusted-host=mirrors.aliyun.com 124 | 125 | 126 | ``` 127 | 128 | ### 安装依赖包 129 | ```commandline 130 | sudo apt install libmysqlclient-dev python3-dev gcc 131 | pip3 install -r requirements 132 | ``` 133 | 134 | ### 创建数据库 135 | 136 | ```sql 137 | CREATE DATABASE `smartpxe` CHARACTER SET 'utf8mb4' 138 | ``` 139 | 140 | ### 迁移数据库 141 | 142 | ```bash 143 | python3 manage.py makemigrations 144 | python3 manage.py migrate 145 | ``` 146 | 147 | ### 创建超级用户 148 | 149 | ```bash 150 | python3 manage.py createsuperuser 151 | ``` 152 | 153 | ### 启动项目 154 | 155 | ```bash 156 | # open dev 157 | smartpxe.settings.py -> CONF_STATUS = 0 158 | python3 manage.py runserver 0.0.0.0:8000 159 | ``` 160 | 161 | ### build 162 | 163 | ```bash 164 | python setup.py sdist --formats=gztar 165 | # dist/SmartPXE-version.tar.gz 166 | ``` 167 | 168 | 169 | ### install 170 | 171 | ```bash 172 | 1.从百度网盘下载 smartpxe_install_require.zip依赖文件 173 | 2.修改install.sh里面的版本号 174 | 3.将软件包放置在install.sh同级目录下 175 | 4.执行安装(root权限) 176 | ``` 177 | 178 | ```angular2html 179 | 链接:https://pan.baidu.com/s/1jJJ0ZMigI7bN_8bnkz1Q-Q?pwd=m3qj 180 | 提取码:m3qj 181 | ``` 182 | 183 | -------------------------------------------------------------------------------- /dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/dashboard/__init__.py -------------------------------------------------------------------------------- /dashboard/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /dashboard/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DashboardConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'dashboard' 7 | -------------------------------------------------------------------------------- /dashboard/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /dashboard/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /dashboard/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from .views import get_sys_info, get_status_info, get_history_info 4 | from rest_framework.routers import SimpleRouter 5 | 6 | router = SimpleRouter() 7 | 8 | urlpatterns = [ 9 | path('sysinfo/', get_sys_info), 10 | path('status/', get_status_info), 11 | path('history/', get_history_info) 12 | ] + router.urls 13 | 14 | 15 | print('=' * 30) 16 | print(urlpatterns) 17 | print('=' * 30) -------------------------------------------------------------------------------- /dashboard/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import psutil 4 | from django.db.models import Count 5 | from rest_framework.decorators import api_view 6 | from rest_framework.request import Request 7 | from rest_framework.response import Response 8 | from install.models import Discover, InstallResult, InstallProgress 9 | 10 | # get sys info 11 | # @permission_classes([IsAuthenticated]) 12 | @api_view(['GET']) 13 | def get_sys_info(request: Request): 14 | options = { 15 | "cpu": psutil.cpu_percent(), 16 | "disk": psutil.virtual_memory().percent, 17 | "mem": psutil.disk_usage('/').percent 18 | } 19 | return Response(options) 20 | 21 | 22 | # get status number 23 | # @permission_classes([IsAuthenticated]) 24 | @api_view(['GET']) 25 | def get_status_info(request: Request): 26 | client_num = Discover.objects.all().count() 27 | install_num = InstallProgress.objects.all().count() 28 | success = InstallResult.objects.filter(status=1).count() 29 | failed = InstallResult.objects.filter(status=0).count() 30 | 31 | status = { 32 | "success": success, 33 | "failed": failed, 34 | "online": client_num, 35 | "running": install_num 36 | } 37 | 38 | return Response(status) 39 | 40 | 41 | @api_view(['GET']) 42 | def get_history_info(request: Request): 43 | """ 44 | 获取完成的,失败和成功的历史数据 45 | :param request: 46 | :return: 47 | """ 48 | end_time = datetime.datetime.now() 49 | start_time = end_time - datetime.timedelta(days=30) 50 | 51 | select = {'day': 'date(date)'} 52 | 53 | success_module = InstallResult.objects.filter(status=1, date__range=(start_time, end_time)).extra(select) 54 | success = success_module.values('day').distinct().order_by('day').annotate(number=Count('date')) 55 | 56 | failed_module = InstallResult.objects.filter(status=0, date__range=(start_time, end_time)).extra(select) 57 | failed = failed_module.values('day').distinct().order_by('day').annotate(number=Count('date')) 58 | 59 | # 循环成功和失败的结果,按照对应的key放在_date_value里面 60 | _date_value_s = {} 61 | _date_value_f = {} 62 | days = [] 63 | 64 | for d in range(30): 65 | day = (end_time + datetime.timedelta(days=(d - 30))).strftime("%Y-%m-%d") 66 | days.append(day) 67 | # 初始化 68 | _date_value_s[day] = 0 69 | _date_value_f[day] = 0 70 | # print(days) 71 | 72 | for i in success: 73 | day = i.get("day").strftime("%Y-%m-%d") 74 | # print(day) 75 | value = i.get("number") 76 | _date_value_s[day] = value 77 | 78 | # print(_date_value_s) 79 | for i in failed: 80 | day = i.get("day").strftime("%Y-%m-%d") 81 | value = i.get("number") 82 | _date_value_f[day] = value 83 | 84 | # a1 = sorted(_date_value_s.items(), key=lambda x: x[0]) 85 | 86 | status = { 87 | "success": [i for i in _date_value_s.values()], 88 | "failed": [i for i in _date_value_f.values()], 89 | "days": days 90 | } 91 | 92 | return Response(status) -------------------------------------------------------------------------------- /frontends/css/app.c916690f.css: -------------------------------------------------------------------------------- 1 | .login_container[data-v-c682aa96]{background-color:#2b4b6b;height:100%}.login_box[data-v-c682aa96]{width:450px;height:300px;background-color:#fff;border-radius:25px;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}.login_box .avatar_box[data-v-c682aa96]{height:130px;width:130px;padding:10px;position:absolute;left:50%;transform:translate(-50%,-50%)}.login_box .avatar_box img[data-v-c682aa96]{width:100%;height:100%;border-radius:15%}.login_box .ms-title[data-v-c682aa96]{width:100%;line-height:150px;text-align:center;font-size:20px;color:#0a0a0a}.login_box .login_form[data-v-c682aa96]{position:absolute;bottom:0;width:100%;padding:0 20px;box-sizing:border-box}.el-container[data-v-f91aab72]{height:100%}.el-header[data-v-f91aab72]{display:flex;justify-content:space-between;align-items:center;padding-left:5px}.el-header .logo[data-v-f91aab72]{display:flex}.el-header .logo img[data-v-f91aab72]{width:30px}.el-header .logo .title[data-v-f91aab72]{font-size:24px;margin-left:5px}.el-header .logo i[data-v-f91aab72]{font-size:30px;margin-left:10px}.el-header .user[data-v-f91aab72]{display:flex}.el-header .user img[data-v-f91aab72]{width:30px}.el-aside[data-v-f91aab72]{background-color:#123}.el-main[data-v-f91aab72]{background-color:#f0f2f5}.el-dropdown-link[data-v-f91aab72]{cursor:pointer;color:#409eff}.el-icon-arrow-down[data-v-f91aab72]{font-size:12px}.el-menu[data-v-f91aab72]{border-right:none}.flex[data-v-af4760b4]{display:flex}.justify-between[data-v-af4760b4]{justify-content:space-between}.icon[data-v-af4760b4]{padding:16px}.icon .i[data-v-af4760b4]{font-size:40px}.data[data-v-af4760b4]{flex-direction:column}.data>span[data-v-af4760b4]{text-align:right;font-size:20px;line-height:1;font-weight:700}[data-v-1d5008b1] .el-descriptions__body .el-descriptions__table{border-collapse:inherit;width:98%}[data-v-1d5008b1] .el-drawer__body{margin-left:2%}.my-label{background:#4bd107}.my-content{background:#fde2fc}[data-v-0a3bcaee] .el-textarea.is-disabled .el-textarea__inner{background-color:#303133;border-color:#e4e7ed;color:#f56c6c;cursor:not-allowed}[data-v-0a3bcaee] .el-descriptions__body .el-descriptions__table{border-collapse:inherit;width:98%}[data-v-0a3bcaee] .el-drawer__body{margin-left:2%}[data-v-3e8af026] .el-descriptions__body .el-descriptions__table{border-collapse:inherit;width:98%}[data-v-3e8af026] .el-drawer__body{margin-left:2%}.el-dialog .el-form-item .el-input[data-v-f2160b62],.el-dialog .el-form-item .el-select[data-v-f2160b62]{width:80%}.el-dialog .el-form .el-form.item .el-textarea__inner[data-v-d3208b7e]{min-height:500px}[data-v-d3208b7e] .el-textarea__inner{background-color:#303133;border-color:#e4e7ed;color:#f4f4f5}[data-v-d3208b7e] .el-textarea.is-disabled .el-textarea__inner{background-color:#303133;border-color:#e4e7ed;color:#f4f4f5;cursor:not-allowed}[data-v-d3208b7e] .el-input.is-disabled .el-input__inner{background-color:#e4e7ed;border-color:#e4e7ed;color:#303133;cursor:not-allowed}.el-dialog .el-form .el-form.item .el-textarea__inner[data-v-3f30137e]{min-height:500px}[data-v-3f30137e] .el-textarea__inner{background-color:#303133;border-color:#e4e7ed;color:#f4f4f5}[data-v-3f30137e] .el-textarea.is-disabled .el-textarea__inner{background-color:#303133;border-color:#e4e7ed;color:#f4f4f5;cursor:not-allowed}[data-v-3f30137e] .el-input.is-disabled .el-input__inner{background-color:#e4e7ed;border-color:#e4e7ed;color:#303133;cursor:not-allowed}.my-label[data-v-b957519c]{background:#4bd107}.my-content[data-v-b957519c]{background:#fde2fc}.console[data-v-b957519c]{width:100%;height:300px;background-color:#000;font-size:15px;padding:5px;color:#fff;white-space:pre-line}[data-v-5d808619] .el-descriptions__body .el-descriptions__table{border-collapse:inherit;width:98%}[data-v-5d808619] .el-drawer__body{margin-left:2%}.console[data-v-5d808619]{width:1200px;height:600px;background-color:#000;font-size:15px;padding:5px;color:#fff;white-space:pre-line}#app,body,html{height:100%;margin:0;padding:0;min-height:200px}.el-breadcrumb{margin-bottom:15px}.el-table{margin:15px 0} -------------------------------------------------------------------------------- /frontends/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/frontends/favicon.ico -------------------------------------------------------------------------------- /frontends/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/frontends/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /frontends/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/frontends/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /frontends/img/logo.89a4c943.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/frontends/img/logo.89a4c943.png -------------------------------------------------------------------------------- /frontends/index.html: -------------------------------------------------------------------------------- 1 | SmartPXE智能部署平台
-------------------------------------------------------------------------------- /frontends/js/app.fae1315c.js: -------------------------------------------------------------------------------- 1 | (function(e){function t(t){for(var n,i,o=t[0],l=t[1],c=t[2],d=0,m=[];d0&&void 0!==e[0]?e[0]:1,n||(n=1),a.next=4,t.$http.get("users/?page=".concat(n));case 4:if(r=a.sent,s=r.data,!s.code){a.next=8;break}return a.abrupt("return",t.$message.error(s.message));case 8:t.userList=s.results,t.pagination=s.pagination;case 10:case"end":return a.stop()}}),a)})))()},handleSizeChange:function(e){},handleCurrentChange:function(e){this.getUserList(e)},resetForm:function(e){this.$refs[e].resetFields()},addUser:function(){var e=this,t="addUser";this.$refs[t].validate(function(){var a=Object(f["a"])(regeneratorRuntime.mark((function a(n){var r,s;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:if(!n){a.next=13;break}return a.next=3,e.$http.post("users/",e.addImageForm);case 3:if(r=a.sent,s=r.data,!s.code){a.next=7;break}return a.abrupt("return",e.$message.error(s.message));case 7:e.$message.success("用户添加成功!"),e.addHwdialogVisible=!1,e.resetForm(t),e.getUserList(),a.next=14;break;case 13:e.$message.error("验证失败,请重新输入");case 14:case"end":return a.stop()}}),a)})));return function(e){return a.apply(this,arguments)}}())}}},$e=Ce,je=Object(l["a"])($e,ke,_e,!1,null,"5c6b6562",null),Fe=je.exports,Ie=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",[a("el-breadcrumb",{attrs:{"separator-class":"el-icon-arrow-right"}},[a("el-breadcrumb-item",{attrs:{to:{path:"/home"}}},[e._v("首页")]),a("el-breadcrumb-item",[e._v("任务管理")]),a("el-breadcrumb-item",[e._v("任务模板")])],1),a("el-card",{staticClass:"box-card"},[a("el-row",{attrs:{gutter:20}},[a("el-col",{attrs:{span:6}},[a("el-input",{attrs:{placeholder:"请输入内容"}},[a("el-button",{attrs:{slot:"append",icon:"el-icon-search"},slot:"append"})],1)],1),a("el-col",{attrs:{span:12}},[a("el-button",{attrs:{type:"primary"},on:{click:function(t){e.addConfigDialogVisible=!0}}},[e._v("添加模板")])],1)],1),a("el-table",{staticStyle:{width:"100%"},attrs:{data:e.configList,border:""}},[a("el-table-column",{attrs:{type:"index",label:"序号",width:"50px"}}),a("el-table-column",{attrs:{prop:"name",label:"模板名称"}}),a("el-table-column",{attrs:{fixed:"right",label:"操作",width:"300px"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("el-button",{attrs:{type:"info",size:"small"},on:{click:function(t){return e.viewHandler(n)}}},[e._v("查看")]),a("el-button",{attrs:{type:"primary",size:"small"},on:{click:function(t){return e.editHandler(n)}}},[e._v("编辑")]),a("el-button",{attrs:{type:"danger",size:"small"},on:{click:function(t){return e.handleDel(n.id)}}},[e._v("删除")])]}}])})],1),a("el-pagination",{attrs:{"current-page":e.pagination.page,"page-size":e.pagination.size,layout:"total, prev, pager, next, jumper",total:e.pagination.total},on:{"size-change":e.handleSizeChange,"current-change":e.handleCurrentChange}})],1),a("el-dialog",{attrs:{title:"增加模板",visible:e.addConfigDialogVisible,"before-close":e.closeHandler,width:"70%"},on:{"update:visible":function(t){e.addConfigDialogVisible=t},close:function(t){return e.resetForm("addConfig")}}},[a("el-form",{ref:"addConfig",attrs:{model:e.addConfigForm,rules:e.addConfigRules,"label-width":"100px",border:""}},[a("el-form-item",{attrs:{label:"模板名称",prop:"name"}},[a("el-input",{staticStyle:{width:"50%"},model:{value:e.addConfigForm.name,callback:function(t){e.$set(e.addConfigForm,"name",t)},expression:"addConfigForm.name"}})],1),a("el-form-item",{attrs:{label:"Playbook",prop:"content"}},[a("el-input",{attrs:{type:"textarea",rows:20},model:{value:e.addConfigForm.content,callback:function(t){e.$set(e.addConfigForm,"content",t)},expression:"addConfigForm.content"}})],1)],1),a("span",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[a("el-button",{on:{click:function(t){e.addConfigDialogVisible=!1}}},[e._v("取 消")]),a("el-button",{attrs:{type:"primary"},on:{click:e.addConfigHandler}},[e._v("确 定")])],1)],1),a("el-dialog",{attrs:{title:"查看模板",visible:e.viewConfigDialogVisible,width:"70%"},on:{"update:visible":function(t){e.viewConfigDialogVisible=t},close:function(t){return e.resetForm("viewConfig")}}},[a("el-form",{ref:"viewConfig",attrs:{model:e.viewConfigForm,"label-width":"100px"}},[a("el-form-item",{attrs:{label:"模板名称"}},[a("el-input",{staticStyle:{width:"50%"},attrs:{disabled:""},model:{value:e.viewConfigForm.name,callback:function(t){e.$set(e.viewConfigForm,"name",t)},expression:"viewConfigForm.name"}})],1),a("el-form-item",{attrs:{label:"Playbook"}},[a("el-input",{attrs:{type:"textarea",disabled:!0,rows:25},model:{value:e.viewConfigForm.content,callback:function(t){e.$set(e.viewConfigForm,"content",t)},expression:"viewConfigForm.content"}})],1)],1),a("span",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[a("el-button",{on:{click:function(t){e.viewConfigDialogVisible=!1}}},[e._v("关 闭")])],1)],1),a("el-dialog",{attrs:{title:"编辑模板",visible:e.editConfigDialogVisible,"before-close":e.closeHandler,width:"70%"},on:{"update:visible":function(t){e.editConfigDialogVisible=t},close:function(t){return e.resetForm("editConfig")}}},[a("el-form",{ref:"editConfig",attrs:{model:e.editConfigForm,rules:e.editConfigRules,"label-width":"100px"}},[a("el-form-item",{attrs:{label:"模板名称",prop:"name"}},[a("el-input",{staticStyle:{width:"50%"},attrs:{disabled:""},model:{value:e.editConfigForm.name,callback:function(t){e.$set(e.editConfigForm,"name",t)},expression:"editConfigForm.name"}})],1),a("el-form-item",{attrs:{label:"Playbook",prop:"content"}},[a("el-input",{attrs:{type:"textarea",rows:20},model:{value:e.editConfigForm.content,callback:function(t){e.$set(e.editConfigForm,"content",t)},expression:"editConfigForm.content"}})],1)],1),a("span",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[a("el-button",{on:{click:function(t){e.editConfigDialogVisible=!1}}},[e._v("取 消")]),a("el-button",{attrs:{type:"primary"},on:{click:function(t){return e.editConfigHandler()}}},[e._v("确 定")])],1)],1)],1)},Se=[],Re={created:function(){this.getConfigList()},data:function(){return{imageList:[],configList:[],addConfigDialogVisible:!1,addConfigForm:{name:"",content:""},addConfigRules:{name:[{required:!0,message:"请输入模板名称",trigger:"blur"},{min:4,max:16,message:"长度在 4 到 16 个字符",trigger:"blur"}],content:[{required:!0,message:"请输入playbook",trigger:"blur"}]},checkTag:0,viewConfigDialogVisible:!1,viewConfigForm:{name:"",content:""},editConfigDialogVisible:!1,editConfigForm:{name:"",content:""},editConfigRules:{content:[{required:!0,message:"请输入配置参数",trigger:"blur"}]},pagination:{page:1,size:5,total:0}}},methods:{handleSizeChange:function(e){},handleCurrentChange:function(e){this.getConfigList(e)},getConfigList:function(e){var t=this;return Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return e||(e=t.pagination.page),a.next=3,t.$http.get("task/template/?page=".concat(e));case 3:if(n=a.sent,r=n.data,!r.code){a.next=7;break}return a.abrupt("return",t.$message.error(r.message));case 7:t.configList=r.results,t.pagination=r.pagination;case 9:case"end":return a.stop()}}),a)})))()},addConfigHandler:function(){var e=this,t="addConfig";this.$refs[t].validate(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:if(!a){t.next=13;break}return t.next=3,e.$http.post("task/template/",e.addConfigForm);case 3:if(n=t.sent,r=n.data,!r.code){t.next=7;break}return t.abrupt("return",e.$message.error(r.message));case 7:e.$message.success("模板添加成功"),e.addConfigDialogVisible=!1,e.checkTag=0,e.getConfigList(),t.next=14;break;case 13:e.$message.error("验证失败,请检查内容!");case 14:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}())},handleImageChange:function(){},resetForm:function(e){this.$refs[e].resetFields()},viewHandler:function(e){this.viewConfigForm=e,this.viewConfigDialogVisible=!0},editHandler:function(e){this.editConfigForm=e,this.editConfigDialogVisible=!0},editConfigHandler:function(){var e=this,t="editConfig";this.$refs[t].validate(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:if(!a){t.next=12;break}return t.next=3,e.$http.patch("task/template/".concat(e.editConfigForm.id,"/"),e.editConfigForm);case 3:if(n=t.sent,r=n.data,!r.code){t.next=7;break}return t.abrupt("return",e.$message.error(r.message));case 7:e.$message.success("模板修改成功"),e.editConfigDialogVisible=!1,e.getConfigList(),t.next=13;break;case 12:e.$message.error("验证失败,请检查内容!");case 13:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}())},closeHandler:function(){var e=this;this.$msgbox.confirm("关闭窗口将会丢失当前内容, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){e.addConfigDialogVisible=!1,e.editConfigDialogVisible=!1,e.$message.success("关闭窗口成功")})).catch((function(){e.$message({type:"info",message:"已取消关闭窗口"})}))},handleDel:function(e){var t=this;this.$msgbox.confirm("此操作将删除该模板, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then(Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$http.delete("task/template/".concat(e,"/"));case 2:if(n=a.sent,r=n.data,!r.code){a.next=6;break}return a.abrupt("return",t.$message.error(r.message));case 6:t.getConfigList(),t.$message.success("模板已成功删除");case 8:case"end":return a.stop()}}),a)})))).catch((function(){t.$message({type:"info",message:"已取消删除"})}))}}},Le=Re,ze=(a("6ed5c"),Object(l["a"])(Le,Ie,Se,!1,null,"3f30137e",null)),Oe=ze.exports,De=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",[a("el-breadcrumb",{attrs:{"separator-class":"el-icon-arrow-right"}},[a("el-breadcrumb-item",{attrs:{to:{path:"/home"}}},[e._v("首页")]),a("el-breadcrumb-item",[e._v("任务管理")]),a("el-breadcrumb-item",[e._v("主机列表")])],1),a("el-card",{staticClass:"box-card"},[a("el-row",{attrs:{gutter:20}},[a("el-col",{attrs:{span:6}},[a("el-input",{attrs:{placeholder:"请输入内容"},model:{value:e.search,callback:function(t){e.search=t},expression:"search"}},[a("el-button",{attrs:{slot:"append",icon:"el-icon-search"},slot:"append"})],1)],1),a("el-col",{attrs:{span:2}},[a("el-button",{attrs:{type:"success"},on:{click:function(t){return e.getHostList()}}},[e._v("手动刷新")])],1)],1),a("el-table",{ref:"multipleTable",staticStyle:{width:"100%"},attrs:{data:e.hostList,border:"","row-key":"mac"},on:{select:e.handleSelectionChange}},[a("el-table-column",{attrs:{type:"selection","reserve-selection":!0,width:"45"}}),a("el-table-column",{attrs:{type:"index",label:"ID"}}),a("el-table-column",{attrs:{prop:"sn",label:"SN",width:"180"}}),a("el-table-column",{attrs:{prop:"vender",label:"生厂商"}}),a("el-table-column",{attrs:{prop:"product",label:"产品型号"}}),a("el-table-column",{attrs:{prop:"clientip",label:"Client IP"}}),a("el-table-column",{attrs:{prop:"ipmi",label:"IPMI IP"}}),a("el-table-column",{attrs:{prop:"join_date",label:"加入时间",width:"180"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("span",[e._v(" "+e._s(e._f("dateFmt")(n.join_date)))])]}}])}),a("el-table-column",{attrs:{fixed:"right",label:"操作",width:"280px"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("el-button",{attrs:{type:"primary",size:"small"},on:{click:function(t){return e.handleCommand(n)}}},[e._v("运行命令")]),a("el-button",{attrs:{type:"success",size:"small"},on:{click:function(t){return e.handlePlaybook(n)}}},[e._v("执行模板")]),a("el-button",{attrs:{type:"danger",size:"small"},on:{click:function(t){return e.handleDel(n)}}},[e._v("删除")])]}}])})],1),a("el-pagination",{attrs:{"current-page":e.pagination.page,"page-size":e.pagination.size,layout:"total, prev, pager, next, jumper",total:e.pagination.total},on:{"size-change":e.handleSizeChange,"current-change":e.handleCurrentChange}}),a("div",{staticStyle:{"margin-top":"20px"}},[a("el-button",{attrs:{type:"success",size:"small"},on:{click:function(t){return e.handleInstall()}}},[e._v("更改到装机列表")]),a("el-button",{attrs:{type:"danger",size:"small",disabled:""}},[e._v("关机")])],1),a("div",{staticClass:"info"},[a("h2",[e._v("这里可以对进入BootOS的主机执行任务")]),a("h2",[e._v("任务包含: ansible模块, playbook")]),a("h2",[e._v("执行任务完成后,依然可以转到安装列表中")]),a("h2",[e._v("命令的执行可以在当前窗口直接获取结果")]),a("h2",[e._v("模板的执行可以需要在记录中查看")])])],1),a("el-dialog",{attrs:{title:"执行命令",visible:e.commandDialogFormVisible,"before-close":e.closeHandler},on:{"update:visible":function(t){e.commandDialogFormVisible=t},close:function(t){return e.resetForm("command")}}},[a("el-form",{ref:"command",attrs:{model:e.commandForm,rules:e.commandRules,"label-width":"100px"}},[a("el-form-item",{attrs:{label:"当前设备"}},[e._v(" "+e._s(e.commandForm.sn)+" ")]),a("el-form-item",{attrs:{label:"模块",prop:"model"}},[a("el-input",{staticStyle:{width:"50%"},attrs:{placeholder:"请输入模块名称"},model:{value:e.commandForm.model,callback:function(t){e.$set(e.commandForm,"model",t)},expression:"commandForm.model"}})],1),a("el-form-item",{attrs:{label:"参数",prop:"args"}},[a("el-input",{staticStyle:{width:"50%"},attrs:{placeholder:"请输入模块参数"},model:{value:e.commandForm.args,callback:function(t){e.$set(e.commandForm,"args",t)},expression:"commandForm.args"}}),a("div",{staticStyle:{"margin-top":"20px"}},[a("el-button",{attrs:{type:"danger"},on:{click:function(t){return e.clear()}}},[e._v("清空显示框")]),a("el-button",{attrs:{type:"primary"},on:{click:function(t){return e.submitCommand()}}},[e._v(e._s(e.loadingtext))])],1)],1)],1),a("div",{staticClass:"console"},e._l(e.outputList,(function(t,n){return a("div",{key:n,staticClass:"output",attrs:{id:"output"}},[a("p",{domProps:{innerHTML:e._s(t)}})])})),0),a("div",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[a("el-button",{attrs:{type:"danger"},on:{click:function(t){e.commandDialogFormVisible=!1}}},[e._v("关 闭")])],1)],1),a("el-dialog",{attrs:{title:"选择模板",visible:e.templateDialogVisible,"before-close":e.closeHandler,width:"30%"},on:{"update:visible":function(t){e.templateDialogVisible=t},close:function(t){return e.resetForm("template")}}},[a("el-form",{ref:"template",attrs:{model:e.templateForm,rules:e.templateRules,"label-width":"100px",border:""}},[a("el-form-item",{attrs:{label:"当前设备"}},[e._v(" "+e._s(e.templateForm.sn)+" ")]),a("el-form-item",{attrs:{label:"模板名称",prop:"tempid"}},[a("el-select",{staticStyle:{width:"50%"},attrs:{placeholder:"请选择即将运行的模板"},model:{value:e.templateForm.tempid,callback:function(t){e.$set(e.templateForm,"tempid",t)},expression:"templateForm.tempid"}},e._l(e.templateList,(function(e){return a("el-option",{key:e.id,attrs:{label:e.name,value:e.id}})})),1)],1)],1),a("span",{staticClass:"dialog-footer",attrs:{slot:"footer"},slot:"footer"},[a("el-button",{on:{click:function(t){e.templateDialogVisible=!1}}},[e._v("取 消")]),a("el-button",{attrs:{type:"primary"},on:{click:function(t){return e.submitPlaybook()}}},[e._v("发送任务")])],1)],1)],1)},He=[],Te=a("1386"),Ve=a.n(Te),Pe={created:function(){this.getHostList(),this.getTemplateList()},data:function(){return{loadingbutton:!1,loadingtext:"发送并执行",hostList:[],multipleSelection:[],selectArray:[],search:null,pagination:{page:1,size:20,total:0},outputList:[],commandDialogFormVisible:!1,commandForm:{sn:"",mac:"",model:"",args:""},commandRules:{model:[{required:!0,message:"请输入模块名称",trigger:"blur"}]},templateList:[],templateForm:{sn:"",mac:"",tempid:""},templateDialogVisible:!1,templateRules:{tempid:[{required:!0,message:"请选择模板",trigger:"change"}]}}},methods:{getTemplateList:function(){var e=this;return Object(f["a"])(regeneratorRuntime.mark((function t(){var a,n;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,e.$http.get("task/template/");case 2:if(a=t.sent,n=a.data,!n.code){t.next=6;break}return t.abrupt("return",e.$message.error(n.message));case 6:e.templateList=n.results;case 7:case"end":return t.stop()}}),t)})))()},resetForm:function(e){this.$refs[e].resetFields(),this.multipleSelection=[],this.outputList=[]},handleSizeChange:function(e){},handleCurrentChange:function(e){this.getHostList(e)},getHostList:function(e){var t=this;return Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return e||(e=t.pagination.page),a.next=3,t.$http.get("task/hostlist/?page=".concat(e));case 3:if(n=a.sent,r=n.data,!r.code){a.next=7;break}return a.abrupt("return",t.$message.error(r.message));case 7:t.hostList=r.results,t.pagination=r.pagination;case 9:case"end":return a.stop()}}),a)})))()},handleSelectionChange:function(e){this.multipleSelection=e},handleCommand:function(e){this.commandForm.sn=e.sn,this.commandForm.mac=e.mac,this.commandDialogFormVisible=!0},submitCommand:function(){var e=this;this.loadingbutton="运行中...";var t="command";this.$refs[t].validate(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r,s,i,o,l;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:if(!a){t.next=19;break}return n=de.a.service({lock:!0,text:"运行中...",spinner:"el-icon-loading",background:"rgba(0, 0, 0, 0.8)",target:document.querySelector(".el-dialog__body")}),t.next=4,e.$http.post("task/hostlist/".concat(e.commandForm.mac,"/command/"),e.commandForm);case 4:if(r=t.sent,s=r.data,!s.code){t.next=9;break}return e.$nextTick((function(){n.close()})),t.abrupt("return",e.$message.error(s.message));case 9:e.$message.success("命令提交成功"),i=new Ve.a,o=s,l=i.ansi_to_html(o),e.outputList.push(l),e.$nextTick((function(){n.close()})),e.loadingbutton=!1,e.loadingtext="提交并运行",t.next=21;break;case 19:e.fullscreenLoading=!1,e.$message.error("验证失败,请检查内容!");case 21:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}())},closeHandler:function(){var e=this;this.$msgbox.confirm("关闭窗口将会丢失当前内容, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then((function(){e.commandDialogFormVisible=!1,e.$message.success("关闭窗口成功")})).catch((function(){e.$message({type:"info",message:"已取消关闭窗口"})}))},clear:function(){this.outputList=[]},handleEdit:function(e){this.editHostForm=e,this.editDialogFormVisible=!0},getConfigList:function(){var e=this;return Object(f["a"])(regeneratorRuntime.mark((function t(){var a,n;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,e.$http.get("temp/config/");case 2:if(a=t.sent,n=a.data,!n.code){t.next=6;break}return t.abrupt("return",e.$message.error(n.message));case 6:e.configList=n.results;case 7:case"end":return t.stop()}}),t)})))()},handlePlaybook:function(e){this.templateForm.sn=e.sn,this.templateForm.mac=e.mac,this.templateDialogVisible=!0},submitPlaybook:function(){var e=this,t="template";this.$refs[t].validate(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:if(!a){t.next=12;break}return t.next=3,e.$http.post("task/hostlist/".concat(e.templateForm.mac,"/playbook/"),e.templateForm);case 3:if(n=t.sent,r=n.data,!r.code){t.next=7;break}return t.abrupt("return",e.$message.error(r.message));case 7:e.templateDialogVisible=!1,e.getHostList(e.pagination.page),e.$message.success(r),t.next=13;break;case 12:e.$message.error("验证失败,请检查内容!");case 13:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}())},editHost:function(){var e=this,t="editHost";this.$refs[t].validate(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:if(!a){t.next=12;break}return t.next=3,e.$http.patch("install/iprelist/".concat(e.editHostForm.mac,"/"),e.editHostForm);case 3:if(n=t.sent,r=n.data,!r.code){t.next=7;break}return t.abrupt("return",e.$message.error(r.message));case 7:e.editDialogFormVisible=!1,e.getHostList(e.pagination.page),e.$message.success("配置成功"),t.next=13;break;case 12:e.$message.error("验证失败,请检查内容!");case 13:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}())},handleDelete:function(){},handlePxeboot:function(){},handleDel:function(e){var t=this;this.$msgbox.confirm("此操作将删除该设备, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then(Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$http.delete("task/hostlist/".concat(e.mac,"/"));case 2:if(n=a.sent,r=n.data,!r.code){a.next=6;break}return a.abrupt("return",t.$message.error(r.message));case 6:t.getHostList(t.pagination.page),t.$message.success("设备"+e.sn+" 已成功删除");case 8:case"end":return a.stop()}}),a)})))).catch((function(){t.$message({type:"info",message:"已取消删除"})}))},toggleSelection:function(e){var t=this;e?e.forEach((function(e){t.$refs.multipleTable.toggleRowSelection(e)})):this.$refs.multipleTable.clearSelection()},handleInstall:function(){var e=this;0!==this.$refs.multipleTable.selection.length?this.$refs.multipleTable.selection.forEach(function(){var t=Object(f["a"])(regeneratorRuntime.mark((function t(a){var n,r;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return t.next=2,e.$http.post("/task/hostlist/".concat(a.mac,"/convert/"));case 2:if(n=t.sent,r=n.data,!r.code){t.next=6;break}return t.abrupt("return",e.$message.error(r.message));case 6:e.$message.success("".concat(a.mac,"已经成功添加到装机列表")),e.$refs.multipleTable.selection.shift(),e.getHostList();case 9:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}()):this.$message.error("当前未选中任何设备")}}},Ee=Pe,Me=(a("1fc8"),Object(l["a"])(Ee,De,He,!1,null,"b957519c",null)),Ue=Me.exports,Be=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",[a("el-breadcrumb",{attrs:{"separator-class":"el-icon-arrow-right"}},[a("el-breadcrumb-item",{attrs:{to:{path:"/home"}}},[e._v("首页")]),a("el-breadcrumb-item",[e._v("任务管理")]),a("el-breadcrumb-item",[e._v("执行结果")])],1),a("el-card",{staticClass:"box-card"},[a("el-row",{attrs:{gutter:20}},[a("el-col",{attrs:{span:6}},[a("el-input",{attrs:{placeholder:"请输入内容"}},[a("el-button",{attrs:{slot:"append",icon:"el-icon-search"},slot:"append"})],1)],1),a("el-col",{attrs:{span:2}},[a("el-button",{attrs:{type:"success"},on:{click:function(t){return e.getList()}}},[e._v("手动刷新")])],1),a("el-col",{attrs:{span:3}},[a("el-button",{attrs:{type:"danger"},on:{click:function(t){return e.cycleRunFunc(0,e.getList)}}},[e._v("停止自动刷新")])],1)],1),a("el-table",{staticStyle:{width:"100%"},attrs:{data:e.tableData,border:""},on:{"selection-change":e.handleSelectionChange}},[a("el-table-column",{attrs:{type:"selection",width:"45"}}),a("el-table-column",{attrs:{type:"index",label:"ID"}}),a("el-table-column",{attrs:{prop:"name",label:"SN",width:"180px"}}),a("el-table-column",{attrs:{prop:"playbook",label:"运行模板"}}),a("el-table-column",{attrs:{prop:"task_id",label:"任务ID"}}),a("el-table-column",{attrs:{prop:"date",label:"开始时间",width:"180"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("span",[e._v(" "+e._s(e._f("dateFmt")(n.date)))])]}}])}),a("el-table-column",{attrs:{prop:"status",label:"任务状态",width:"100",filters:[{text:"正在运行中",value:1},{text:"任务结束",value:0}],"filter-method":e.filterTag,"filter-placement":"bottom-end"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("el-tag",{attrs:{type:n.status?"primary":"danger","disable-transitions":""}},[e._v(e._s(n.status?"Running":"Done"))])]}}])}),a("el-table-column",{attrs:{fixed:"right",label:"操作",width:"200px"},scopedSlots:e._u([{key:"default",fn:function(t){var n=t.row;return[a("el-button",{attrs:{type:"info",size:"small"},on:{click:function(t){return e.handleInfo(n)}}},[e._v("任务详情")]),a("el-button",{attrs:{type:"danger",size:"small"},on:{click:function(t){return e.handleDel(n.id)}}},[e._v("删除")])]}}])})],1),a("el-pagination",{attrs:{"current-page":e.pagination.page,"page-size":e.pagination.size,layout:"total, prev, pager, next, jumper",total:e.pagination.total},on:{"size-change":e.handleSizeChange,"current-change":e.handleCurrentChange}}),a("div",{staticStyle:{"margin-top":"20px"}},[a("el-button",{attrs:{type:"info",size:"small",disabled:""}},[e._v("信息导出")])],1)],1),a("el-drawer",{ref:"drawer",attrs:{title:e.tempInfoObj.name+" 的详细信息","before-close":e.handleClose,visible:e.dialog,direction:"rtl",size:"70%","custom-class":"demo-drawer",border:!0},on:{"update:visible":function(t){e.dialog=t}}},[a("div",{staticClass:"demo-drawer__content"},[a("el-descriptions",{staticClass:"margin-top",attrs:{title:"任务详情",column:1,border:"",size:"small"}},[a("el-descriptions-item",{attrs:{label:"当前状态"}},[a("el-steps",{attrs:{space:200,active:parseInt(e.tempInfoObj.progress),"finish-status":"success"}},[a("el-step",{attrs:{title:"任务开始"}}),a("el-step",{attrs:{title:"执行中"}}),a("el-step",{attrs:{title:"任务结束"}})],1)],1),a("el-descriptions-item",{attrs:{label:"实时日志"}},[a("div",{staticClass:"console"},e._l(e.outputList,(function(t,n){return a("div",{key:n,staticClass:"output",attrs:{id:"output"}},[a("p",{domProps:{innerHTML:e._s(t)}})])})),0)])],1),a("div",{staticClass:"demo-drawer__footer"})],1)])],1)},qe=[],Ae={created:function(){this.getList(),this.cycleRunFunc(1,this.getList,5e3)},beforeDestroy:function(){for(var e in this.timerManager)clearInterval(this.timerManager[e])},data:function(){return{pagination:{page:1,size:20,total:0},table:!1,dialog:!1,loading:!1,formLabelWidth:"200px",timer:null,size:"",outputList:[],tempInfoObj:{name:"",progress:"",result:"",taskid:""},tableData:[],whileTag:1,intervalID:null,timerManager:{}}},methods:{getList:function(e){var t=this;return Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return e||(e=t.pagination.page),a.next=3,t.$http.get("task/result/?page=".concat(e));case 3:if(n=a.sent,r=n.data,!r.code){a.next=7;break}return a.abrupt("return",t.$message.error(r.message));case 7:t.tableData=r.results,t.pagination=r.pagination;case 9:case"end":return a.stop()}}),a)})))()},filterTag:function(e,t){return t.status===e},filterHandler:function(e,t,a){var n=a.property;return t[n]===e},handleSizeChange:function(e){},handleCurrentChange:function(e){this.getList(e)},handleClose:function(){this.getList(),this.dialog=!1,this.cycleRunFunc(0,this.getRunningResult),this.tempInfoObj={name:"",progress:"",result:"",taskid:""},this.outputList=[]},cycleRunFunc:function(e,t,a){var n=t.name;if(e){this.$message.success("自动刷新已经开始");var r=setInterval(t,a);this.timerManager[n]=r}else{var s=this.timerManager[n];clearInterval(s),this.timerManager[n]=void 0,this.$message.success("已停止自动刷新")}},handleInfo:function(e){if(e.status)this.dialog=!0,this.tempInfoObj.taskid=e.task_id,this.tempInfoObj.progress=e.progress,this.cycleRunFunc(1,this.getRunningResult,3e3);else{this.tempInfoObj=e;var t=new Ve.a,a=e.result,n=t.ansi_to_html(a);this.outputList.push(n),this.dialog=!0}},getRunningResult:function(){var e=this;return Object(f["a"])(regeneratorRuntime.mark((function t(){var a,n,r,s,i,o;return regeneratorRuntime.wrap((function(t){while(1)switch(t.prev=t.next){case 0:return e.outputList=[],a=e.tempInfoObj.taskid,t.next=4,e.$http.get("task/result/running/?taskid=".concat(a));case 4:if(n=t.sent,r=n.data,!r.code){t.next=11;break}if(888!==r.code){t.next=10;break}return e.cycleRunFunc(0,e.getRunningResult),t.abrupt("return",e.$message.error(r.message));case 10:return t.abrupt("return",e.$message.error(r.message));case 11:s=new Ve.a,i=r,o=s.ansi_to_html(i),e.outputList.push(o);case 15:case"end":return t.stop()}}),t)})))()},handlePxeboot:function(){},handleDel:function(e){var t=this;this.$msgbox.confirm("此操作将删除该记录, 是否继续?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then(Object(f["a"])(regeneratorRuntime.mark((function a(){var n,r;return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$http.delete("task/result/".concat(e,"/"));case 2:if(n=a.sent,r=n.data,!r.code){a.next=6;break}return a.abrupt("return",t.$message.error(r.message));case 6:t.getList(),t.$message.success("记录已成功删除");case 8:case"end":return a.stop()}}),a)})))).catch((function(){t.$message({type:"info",message:"已取消删除"})}))},toggleSelection:function(e){var t=this;e?e.forEach((function(e){t.$refs.multipleTable.toggleRowSelection(e)})):this.$refs.multipleTable.clearSelection()},handleSelectionChange:function(e){this.multipleSelection=e}}},Ne=Ae,Ge=(a("e6c2"),Object(l["a"])(Ne,Be,qe,!1,null,"5d808619",null)),Je=Ge.exports;n["default"].use(d["a"]);var Ke=[{path:"/",redirect:"/home"},{path:"/login",component:v},{path:"/home",component:C,redirect:"/dashboard",children:[{path:"/dashboard",component:L},{path:"/discovered",component:V},{path:"/presettings",component:q},{path:"/prelist",component:W},{path:"/installing",component:te},{path:"/installed",component:oe},{path:"/images",component:ge},{path:"/ostemps",component:ye},{path:"/hwtemps",component:Fe},{path:"/hostlist",component:Ue},{path:"/tasktemps",component:Oe},{path:"/taskresult",component:Je}]}],We=new d["a"]({routes:Ke});We.beforeEach((function(e,t,a){if("/login"===e.path)a();else{var n=window.localStorage.getItem("token");n?a():a("/login")}}));var Xe=We,Qe=(a("9e1f"),a("6ed5")),Ye=a.n(Qe),Ze=(a("0fb7"),a("f529")),et=a.n(Ze),tt=(a("b5d8"),a("f494")),at=a.n(tt),nt=(a("6611"),a("e772")),rt=a.n(nt),st=(a("826b"),a("c263")),it=a.n(st),ot=(a("4ffc"),a("946e")),lt=a.n(ot),ct=(a("e960"),a("b35b")),ut=a.n(ct),dt=(a("d4df"),a("7fc1")),mt=a.n(dt),pt=(a("560b"),a("dcdc")),ft=a.n(pt),gt=(a("d96c"),a("0c9b")),bt=a.n(gt),ht=(a("fb08"),a("21e5")),vt=a.n(ht),wt=(a("0fb4"),a("9944")),xt=a.n(wt),yt=(a("f225"),a("89a9")),kt=a.n(yt),_t=(a("672e"),a("101e")),Ct=a.n(_t),$t=(a("a7cc"),a("df33")),jt=a.n($t),Ft=(a("f4f9"),a("c2cc")),It=a.n(Ft),St=(a("7a0f"),a("0f6c")),Rt=a.n(St),Lt=(a("5466"),a("ecdf")),zt=a.n(Lt),Ot=(a("38a0"),a("ad41")),Dt=a.n(Ot),Ht=(a("b8e0"),a("a4c4")),Tt=a.n(Ht),Vt=(a("b84d"),a("c216")),Pt=a.n(Vt),Et=(a("8f24"),a("76b9")),Mt=a.n(Et),Ut=(a("4ca3"),a("443e")),Bt=a.n(Ut),qt=(a("8bd8"),a("4cb2")),At=a.n(qt),Nt=(a("ce18"),a("f58e")),Gt=a.n(Nt),Jt=(a("a769"),a("5cc3")),Kt=a.n(Jt),Wt=(a("de31"),a("c69e")),Xt=a.n(Wt),Qt=(a("a673"),a("7b31")),Yt=a.n(Qt),Zt=(a("adec"),a("3d2d")),ea=a.n(Zt),ta=(a("10cb"),a("f3ad")),aa=a.n(ta),na=(a("eca7"),a("3787")),ra=a.n(na),sa=(a("425f"),a("4105")),ia=a.n(sa),oa=(a("1951"),a("eedf")),la=a.n(oa),ca=(a("fe07"),a("6ac5")),ua=a.n(ca),da=(a("34db"),a("3803")),ma=a.n(da),pa=(a("1f1a"),a("4e4b")),fa=a.n(pa),ga=(a("6b30"),a("c284")),ba=a.n(ga),ha=(a("d2ac"),a("95b0")),va=a.n(ha),wa=(a("9c49"),a("6640")),xa=a.n(wa),ya=(a("cbb5"),a("8bbc")),ka=a.n(ya),_a=(a("cb70"),a("b370")),Ca=a.n(_a),$a=(a("960d"),a("defb")),ja=a.n($a),Fa=(a("bd49"),a("18ff")),Ia=a.n(Fa),Sa=(a("06f1"),a("6ac9")),Ra=a.n(Sa),La=(a("fd71"),a("a447")),za=a.n(La);a("0fae");n["default"].use(za.a),n["default"].use(Ra.a),n["default"].use(Ia.a),n["default"].use(ja.a),n["default"].use(Ca.a),n["default"].use(ka.a),n["default"].use(xa.a),n["default"].use(va.a),n["default"].use(ba.a),n["default"].use(fa.a),n["default"].use(ma.a),n["default"].use(ua.a),n["default"].use(la.a),n["default"].use(ia.a),n["default"].use(ra.a),n["default"].use(aa.a),n["default"].use(ea.a),n["default"].use(Yt.a),n["default"].use(Xt.a),n["default"].use(Kt.a),n["default"].use(Gt.a),n["default"].use(At.a),n["default"].use(Bt.a),n["default"].use(Mt.a),n["default"].use(Pt.a),n["default"].use(Tt.a),n["default"].use(Dt.a),n["default"].use(zt.a),n["default"].use(Rt.a),n["default"].use(It.a),n["default"].use(jt.a),n["default"].use(Ct.a),n["default"].use(kt.a),n["default"].use(xt.a),n["default"].use(vt.a),n["default"].use(bt.a),n["default"].use(ft.a),n["default"].use(mt.a),n["default"].use(ut.a),n["default"].use(lt.a),n["default"].use(it.a),n["default"].use(rt.a),n["default"].use(at.a),n["default"].prototype.$message=et.a,n["default"].prototype.$msgbox=Ye.a;a("5aea");var Oa=a("bc3a"),Da=a.n(Oa),Ha=a("c1df"),Ta=a.n(Ha);Ta.a.locale("zh-cn"),Da.a.interceptors.request.use((function(e){return e.headers.Authorization="Bearer "+window.localStorage.getItem("token"),e})),Da.a.interceptors.response.use((function(e){if(!(e.data.code&&e.data.code<100))return e;n["default"].prototype.$message.error(e.data.message),Xe.push("/login")})),Da.a.defaults.baseURL="/api/v1/",n["default"].prototype.$http=Da.a,n["default"].prototype.$echarts=F,n["default"].config.productionTip=!1,n["default"].filter("dateFmt",(function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"lll";return Ta()(e).format(t)})),new n["default"]({router:Xe,render:function(e){return e(u)}}).$mount("#app")},"578d":function(e,t,a){},"5aea":function(e,t,a){},"5d82":function(e,t,a){},"668c":function(e,t,a){},"6ed5c":function(e,t,a){"use strict";a("578d")},8378:function(e,t,a){"use strict";a("e89a")},"843d":function(e,t,a){"use strict";a("668c")},9156:function(e,t,a){},aefc:function(e,t,a){},b783:function(e,t,a){},b935:function(e,t,a){"use strict";a("9156")},cf05:function(e,t,a){e.exports=a.p+"img/logo.89a4c943.png"},d211:function(e,t,a){},dea4:function(e,t,a){"use strict";a("5d82")},df29:function(e,t,a){},e6c2:function(e,t,a){"use strict";a("1576")},e89a:function(e,t,a){},eea9:function(e,t,a){"use strict";a("df29")}}); 2 | //# sourceMappingURL=app.fae1315c.js.map -------------------------------------------------------------------------------- /install/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/install/__init__.py -------------------------------------------------------------------------------- /install/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /install/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InstallConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'install' 7 | -------------------------------------------------------------------------------- /install/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/install/migrations/__init__.py -------------------------------------------------------------------------------- /install/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Create your models here. 5 | 6 | # 发现设备列表 7 | class Discover(models.Model): 8 | isVM = models.BooleanField(verbose_name="是否为虚拟机") 9 | sn = models.CharField(blank=False, max_length=20, verbose_name="SN") 10 | mac = models.CharField(blank=False, max_length=20, verbose_name="MAC地址", primary_key=True) 11 | vender = models.CharField(blank=True, null=True, max_length=200, verbose_name='厂家') 12 | product = models.CharField(blank=True, null=True, max_length=200, verbose_name='产品') 13 | cpuinfo = models.CharField(blank=True, null=True, max_length=200, verbose_name='CPU') 14 | meminfo = models.CharField(blank=True, null=True, max_length=200, verbose_name='内存') 15 | status = models.BooleanField(default=True) 16 | clientip = models.GenericIPAddressField(blank=False, verbose_name="Client IP") 17 | ipmi = models.GenericIPAddressField(blank=True, null=True, verbose_name="IPMI IP") 18 | date = models.DateTimeField(blank=False, verbose_name="加入时间", auto_now=True) 19 | sysinfo = models.JSONField(verbose_name="系统信息") 20 | 21 | class Meta: 22 | verbose_name = '发现设备列表' 23 | db_table = 'p_discover_list' 24 | ordering = ['date'] 25 | 26 | def __str__(self): 27 | return self.sn 28 | 29 | 30 | # 准备安装列表 31 | class InstallPreList(models.Model): 32 | isVM = models.BooleanField(verbose_name="是否为虚拟机") 33 | sn = models.CharField(blank=False, max_length=20, verbose_name="SN") 34 | mac = models.CharField(blank=False, max_length=20, verbose_name="MAC地址", primary_key=True) 35 | vender = models.CharField(blank=True, null=True, max_length=200, verbose_name='厂家') 36 | product = models.CharField(blank=True, null=True, max_length=200, verbose_name='产品') 37 | os = models.CharField(blank=True, null=True, max_length=100, verbose_name='系统') 38 | config = models.CharField(blank=True, null=True, max_length=100, verbose_name='配置') 39 | # status 代表当前状态, 0: 未安装——正在准备, 1: 正在安装 40 | status = models.BooleanField(default=False) 41 | clientip = models.GenericIPAddressField(blank=False, verbose_name="Client IP") 42 | ipmi = models.GenericIPAddressField(blank=True, null=True, verbose_name="IPMI IP") 43 | pxe_menu_path = models.CharField(blank=True, null=True, max_length=250, verbose_name='启动菜单位置') 44 | join_date = models.DateTimeField(blank=False, verbose_name="加入时间", auto_now_add=True) 45 | sysinfo = models.JSONField(verbose_name="系统信息") 46 | 47 | class Meta: 48 | verbose_name = '发现设备列表' 49 | db_table = 'p_install_pre_list' 50 | ordering = ['join_date'] 51 | 52 | def __str__(self): 53 | return self.sn 54 | 55 | 56 | # 安装进度表 57 | # 先存在redis中? 等安装完成后,统一写入数据库,建立一个完成表? 后续做 58 | class InstallProgress(models.Model): 59 | sn = models.CharField(blank=False, max_length=20, verbose_name="SN") 60 | mac = models.CharField(blank=False, max_length=20, verbose_name="MAC地址", primary_key=True) 61 | vender = models.CharField(blank=True, null=True, max_length=200, verbose_name='厂家') 62 | product = models.CharField(blank=True, null=True, max_length=200, verbose_name='产品') 63 | os = models.CharField(blank=True, null=True, max_length=100, verbose_name='镜像') 64 | config = models.CharField(blank=True, null=True, max_length=100, verbose_name='配置') 65 | clientip = models.GenericIPAddressField(blank=False, verbose_name="Client IP") 66 | ipmi = models.GenericIPAddressField(blank=True, null=True, verbose_name="IPMI IP") 67 | install_date = models.DateTimeField(blank=False, verbose_name="安装时间", auto_now_add=True) 68 | status_id = models.CharField(max_length=20, verbose_name='最新进度状态') 69 | status_progress = models.CharField(max_length=5, verbose_name='最新进度状态') 70 | status_content = models.TextField(max_length=10086, verbose_name='所有进度信息') 71 | pxe_menu_path = models.CharField(blank=True, null=True, max_length=250, verbose_name='启动菜单位置') 72 | 73 | class Meta: 74 | verbose_name = '安装进度表' 75 | ordering = ['install_date'] 76 | db_table = 'p_install_progress' 77 | 78 | 79 | # 结果存储表 80 | # 先存在redis中? 等安装完成后,统一写入数据库,建立一个完成表? 后续做 81 | class InstallResult(models.Model): 82 | sn = models.CharField(blank=False, max_length=20, verbose_name="SN") 83 | mac = models.CharField(blank=False, max_length=20, verbose_name="MAC地址") 84 | vender = models.CharField(blank=True, null=True, max_length=200, verbose_name='厂家') 85 | product = models.CharField(blank=True, null=True, max_length=200, verbose_name='产品') 86 | os = models.CharField(blank=True, null=True, max_length=100, verbose_name='镜像') 87 | config = models.CharField(blank=True, null=True, max_length=100, verbose_name='配置') 88 | clientip = models.GenericIPAddressField(blank=False, verbose_name="Client IP") 89 | ipmi = models.GenericIPAddressField(blank=True, null=True, verbose_name="IPMI IP") 90 | date = models.DateTimeField(blank=False, verbose_name="完成时间", auto_now_add=True) 91 | status = models.BooleanField() 92 | status_id = models.CharField(max_length=20, verbose_name='最新进度状态') 93 | status_progress = models.CharField(max_length=5, verbose_name='最新进度状态') 94 | status_content = models.TextField(max_length=10086, verbose_name='所有进度信息') 95 | pxe_menu_path = models.CharField(blank=True, null=True, max_length=250, verbose_name='启动菜单位置') 96 | 97 | class Meta: 98 | verbose_name = '结果存储表' 99 | db_table = 'p_install_result' 100 | ordering = ['id'] 101 | 102 | # 发现设备列表 103 | # class Install(models.Model): 104 | # sn = models.CharField('SN序列号', max_length=20) 105 | # mac = models.CharField('MAC地址', max_length=20) 106 | # info = models.CharField('设备信息', max_length=500) 107 | # status = models.CharField('当前状态', max_length=20) 108 | # join_date = models.DateTimeField('加入时间', auto_now_add=True) 109 | # last_date = models.DateTimeField('修改时间', auto_now=True) 110 | # 111 | # class Meta: 112 | # verbose_name = '发现设备列表' 113 | # 114 | # def __str__(self): 115 | # return self.sn 116 | 117 | 118 | # # 安装->设备状态 119 | # class IStatus(models.Model): 120 | # sn = models.CharField('SN序列号', max_length=20) 121 | # mac = models.CharField('MAC地址', max_length=20) 122 | # os_temps = models.CharField('系统模板', max_length=20) 123 | # hw_temps = models.CharField('硬件模板', max_length=20) 124 | # ipaddr = models.CharField('设备IP', max_length=100) 125 | # status = models.CharField('当前状态', max_length=20) 126 | # vnc = models.CharField('VNC链接', max_length=100) 127 | # 128 | # class Meta: 129 | # verbose_name = '安装状态' 130 | # 131 | # def __str__(self): 132 | # return self.sn 133 | -------------------------------------------------------------------------------- /install/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Discover, InstallPreList, InstallProgress, InstallResult 3 | 4 | 5 | class DiscoverSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Discover 8 | fields = '__all__' 9 | 10 | 11 | class InstallPreListSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = InstallPreList 14 | fields = '__all__' 15 | 16 | 17 | class InstallProgressSerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = InstallProgress 20 | fields = '__all__' 21 | 22 | 23 | class InstallResultSerializer(serializers.ModelSerializer): 24 | class Meta: 25 | model = InstallResult 26 | fields = '__all__' 27 | 28 | 29 | # class IStatusSerializer(serializers.ModelSerializer): 30 | # class Meta: 31 | # model = IStatus 32 | # fields = '__all__' 33 | 34 | -------------------------------------------------------------------------------- /install/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /install/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from install.views import DiscoverViewSet, InstallPreListViewSet, InstallProgressViewSet, InstallResultViewSet 4 | from rest_framework.routers import SimpleRouter 5 | 6 | router = SimpleRouter() 7 | router.register('discover', DiscoverViewSet) 8 | router.register('iprelist', InstallPreListViewSet) 9 | router.register('progress', InstallProgressViewSet) 10 | router.register('result', InstallResultViewSet) 11 | 12 | urlpatterns = [ 13 | ] + router.urls 14 | 15 | 16 | print('=' * 30) 17 | print(urlpatterns) 18 | print('=' * 30) -------------------------------------------------------------------------------- /install/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | from rest_framework.views import APIView 5 | from rest_framework.decorators import api_view 6 | from rest_framework.views import Response, Request 7 | from utils.tools import generate_pxe_menu, send_run_rom_scripts, ManagerDnsmasq 8 | 9 | from rest_framework.viewsets import ModelViewSet 10 | from .serializers import DiscoverSerializer, InstallPreListSerializer, InstallProgressSerializer, InstallResultSerializer 11 | from .models import Discover, InstallPreList, InstallProgress, InstallResult 12 | from rest_framework.decorators import action 13 | from django.db import transaction 14 | from task.serializers import TaskListSerializer 15 | from django_filters.rest_framework import DjangoFilterBackend 16 | from datetime import datetime 17 | from pathlib import Path 18 | import os 19 | from task.models import TaskList 20 | from task.serializers import TaskListSerializer 21 | 22 | 23 | class DiscoverViewSet(ModelViewSet): 24 | queryset = Discover.objects.all() 25 | serializer_class = DiscoverSerializer 26 | 27 | @action(methods=['POST'], detail=True, url_path='convert/(?P\d+)') 28 | def host_status_convert(self, request:Request, pk, target): 29 | # 这个是转换的api,发送 要转换的pk 和 要转换到哪个表 30 | print(target) # 1: task_pre , 2: install_pre 31 | obj = self.get_object() 32 | serializer = DiscoverSerializer(obj) 33 | data = serializer.data 34 | print('=' * 30) 35 | print(target, type(target)) 36 | if target == '2': 37 | print('~' * 30) 38 | toserializer = InstallPreListSerializer(data=data) 39 | validated = toserializer.is_valid(raise_exception=True) 40 | toserializer.save() 41 | print(toserializer.data) 42 | obj.delete() 43 | if target == '1': 44 | toserializer = TaskListSerializer(data=data) 45 | validated = toserializer.is_valid(raise_exception=True) 46 | toserializer.save() 47 | obj.delete() 48 | return Response(status=201) 49 | 50 | 51 | # class IStatusViewSet(ModelViewSet): 52 | # queryset = IStatus.objects.all() 53 | # serializer_class = IStatusSerializer 54 | 55 | class InstallPreListViewSet(ModelViewSet): 56 | queryset = InstallPreList.objects.all() 57 | serializer_class = InstallPreListSerializer 58 | filter_backends = [DjangoFilterBackend] 59 | filterset_fields = ['status'] 60 | 61 | @action(methods=['POST'], detail=True, url_path='convert') 62 | def convert_to_install(self, request:Request, pk): 63 | # 这个是转换的api,发送 要转换的pk 和 要转换到install_pre 64 | with transaction.atomic(): 65 | obj = self.get_object() 66 | serializer = InstallPreListSerializer(obj) 67 | data = serializer.data 68 | toserializer = TaskListSerializer(data=data) 69 | validated = toserializer.is_valid(raise_exception=True) 70 | toserializer.save() 71 | obj.delete() 72 | return Response(status=201) 73 | 74 | 75 | @action(detail=False, url_path='allmac') 76 | def get_all_mac(self, request): 77 | all_obj = self.get_queryset().filter().values('sn', 'mac', 'os', 'config') 78 | print(all_obj) 79 | 80 | # serializer = InstallPreListSerializer(all_obj) 81 | # data = serializer.data 82 | # print(data) 83 | return Response(all_obj) 84 | 85 | # 拦截patch方法,修改后,根据条目里的config 生成pxe菜单,再填充数据库 86 | 87 | def partial_update(self, request: Request, *args, **kwargs): 88 | # 在这里生成pxe菜单 89 | print(request.data) 90 | print(kwargs) 91 | _config = request.data['config'] 92 | if isinstance(_config, str): 93 | from temp.serializers import CustomOSTempSerializer 94 | from temp.models import CustomOSTemp 95 | config_obj = CustomOSTemp.objects.filter(name=_config).values('image', 'path', 'name')[0] 96 | else: 97 | config_obj = _config 98 | ks_path = config_obj['path'] 99 | image = config_obj['image'] 100 | ks_name = config_obj['name'] 101 | from temp.models import ImageTemp 102 | # all_obj = self.get_queryset().all().values('sn', 'mac', 'os', 'config') 103 | # print(all_obj) 104 | get_image_path = ImageTemp.objects.filter(name=image).values('name', 'path') 105 | image_info = get_image_path[0] 106 | image_kernel = image_info['path'] + '/isolinux/vmlinuz' 107 | image_initrd = image_info['path'] + '/isolinux/initrd.img' 108 | image_name = image_info['name'] 109 | print(image_kernel, image_initrd, image_name, ks_path) 110 | pxe_menu_path = generate_pxe_menu(kwargs['pk'], image_kernel, image_initrd, ks_path, option='') 111 | 112 | path_info = { 113 | 'os': image_name, 114 | 'config': ks_name, 115 | 'pxe_menu_path': pxe_menu_path 116 | } 117 | print('~' * 30) 118 | class NewRequest: 119 | def __init__(self, data): 120 | self.data = data 121 | 122 | newrequest = NewRequest(path_info) 123 | print(newrequest, newrequest.data) 124 | 125 | return super().partial_update(newrequest, *args, **kwargs) 126 | 127 | @action(methods=['POST'], detail=True, url_path='install') 128 | def host_status_to_install(self, request: Request, pk): 129 | # 修改主机状态为安装状态,并且添加mac地址绑定ip 130 | obj = self.get_object() 131 | serializer = InstallPreListSerializer(obj) 132 | data = serializer.data 133 | if data['os'] is None or data['config'] is None: 134 | return Response({'code': 800, 'message': '该设备未关联模板'}) 135 | obj_ip = data['clientip'] 136 | data['status_id'] = '1' 137 | data['status_progress'] = '5%' 138 | data['status_content'] = "[{}] - [{}]".format(datetime.now(), '向客户端发送指令成功') 139 | print(data) 140 | toserializer = InstallProgressSerializer(data=data) 141 | if toserializer.is_valid(raise_exception=True): 142 | toserializer.save() 143 | ManagerDnsmasq().add(data['mac'], obj_ip) 144 | obj.delete() 145 | return Response(status=201) 146 | 147 | def destroy(self, request, *args, **kwargs): 148 | # 删除记录时应该判断一下菜单文件是否存在,如果存在应该将菜单文件一起删除 149 | instance = self.get_object() 150 | serializer = self.get_serializer(instance) 151 | menu_path = serializer.data['pxe_menu_path'] 152 | print(menu_path) 153 | if menu_path and Path(menu_path).exists(): 154 | os.remove(Path(menu_path)) 155 | self.perform_destroy(instance) 156 | return Response(status=204) 157 | 158 | 159 | class InstallProgressViewSet(ModelViewSet): 160 | queryset = InstallProgress.objects.all() 161 | serializer_class = InstallProgressSerializer 162 | filter_backends = [DjangoFilterBackend] 163 | filterset_fields = ['mac'] 164 | 165 | def update(self, request, *args, **kwargs): 166 | partial = kwargs.pop('partial', False) 167 | instance = self.get_object() 168 | old_serializer = self.get_serializer(instance) 169 | old_data = old_serializer.data 170 | print(old_data) 171 | old_content = old_data['status_content'] + '\n' 172 | _new_content = "[{}] - [{}]".format(datetime.now(), request.data['status_content']) 173 | new_content = old_content + _new_content 174 | request.data['status_content'] = new_content 175 | serializer = self.get_serializer(instance, data=request.data, partial=partial) 176 | serializer.is_valid(raise_exception=True) 177 | self.perform_update(serializer) 178 | 179 | if getattr(instance, '_prefetched_objects_cache', None): 180 | # If 'prefetch_related' has been applied to a queryset, we need to 181 | # forcibly invalidate the prefetch cache on the instance. 182 | instance._prefetched_objects_cache = {} 183 | 184 | return Response(serializer.data) 185 | 186 | @action(methods=['POST'], detail=True, url_path='finished/(?P\d+)') 187 | def finished(self, request, pk, status): 188 | # 进度完成接口,将该条记录迁移至安装记录中,并增加安装状态 189 | obj = self.get_object() 190 | serializer = InstallProgressSerializer(obj) 191 | data = serializer.data 192 | print(data) 193 | data['status'] = status 194 | print(status) 195 | toserializer = InstallResultSerializer(data=data) 196 | if toserializer.is_valid(raise_exception=True): 197 | toserializer.save() 198 | mac = data['mac'] 199 | ManagerDnsmasq().delete(mac) 200 | menu_path = data['pxe_menu_path'] 201 | if menu_path and Path(menu_path).exists(): 202 | os.remove(Path(menu_path)) 203 | obj.delete() 204 | return Response(status=201) 205 | 206 | @action(methods=['POST'], detail=True, url_path='termination/(?P\d+)') 207 | def termination(self, request, pk, status): 208 | # 终止安装,将该条记录迁移至安装记录中,并增加终止状态 209 | # 0:终止安装,并关机; 1:终止安装,并重启 210 | obj = self.get_object() 211 | serializer = InstallProgressSerializer(obj) 212 | data = serializer.data 213 | data['status'] = 0 214 | print(status) 215 | new_content = data['status_content'] + '\n' + "[{}] - [{}]".format(datetime.now(), "操作手动终止!") 216 | data['status_content'] = new_content 217 | if status: 218 | send_run_rom_scripts(data['clientip'], "reboot", "root", '') 219 | else: 220 | send_run_rom_scripts(data['clientip'], "showdown", "root", '') 221 | toserializer = InstallResultSerializer(data=data) 222 | if toserializer.is_valid(raise_exception=True): 223 | toserializer.save() 224 | mac = data['mac'] 225 | ManagerDnsmasq().delete(mac) 226 | menu_path = data['pxe_menu_path'] 227 | if menu_path and Path(menu_path).exists(): 228 | os.remove(Path(menu_path)) 229 | obj.delete() 230 | return Response(status=201) 231 | 232 | def destroy(self, request, *args, **kwargs): 233 | """ 234 | 处理手动删除记录 235 | """ 236 | obj = self.get_object() 237 | serializer = InstallProgressSerializer(obj) 238 | data = serializer.data 239 | data['status'] = 0 240 | new_content = data['status_content'] + '\n' + "[{}] - [{}]".format(datetime.now(), "手动删除记录!") 241 | data['status_content'] = new_content 242 | toserializer = InstallResultSerializer(data=data) 243 | if toserializer.is_valid(raise_exception=True): 244 | toserializer.save() 245 | mac = data['mac'] 246 | ManagerDnsmasq().delete(mac) 247 | menu_path = data['pxe_menu_path'] 248 | if menu_path and Path(menu_path).exists(): 249 | os.remove(Path(menu_path)) 250 | obj.delete() 251 | return Response(status=201) 252 | 253 | 254 | 255 | 256 | class InstallResultViewSet(ModelViewSet): 257 | queryset = InstallResult.objects.all() 258 | serializer_class = InstallResultSerializer 259 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | 8 | def main(): 9 | """Run administrative tasks.""" 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartpxe.settings') 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /requirements: -------------------------------------------------------------------------------- 1 | amqp==5.0.9 2 | ansible==5.4.0 3 | ansible-core==2.12.3 4 | ansible-runner==2.1.2 5 | asgiref==3.4.1 6 | asyncio==3.4.3 7 | bcrypt==3.2.0 8 | billiard==3.6.4.0 9 | celery==5.2.3 10 | certifi==2021.5.30 11 | cffi==1.15.0 12 | charset-normalizer==2.0.6 13 | click==8.0.4 14 | click-didyoumean==0.3.0 15 | click-plugins==1.1.1 16 | click-repl==0.2.0 17 | cryptography==36.0.0 18 | Deprecated==1.2.13 19 | Django==3.2.7 20 | django-filter==21.1 21 | djangorestframework==3.12.4 22 | djangorestframework-simplejwt==4.8.0 23 | docutils==0.18.1 24 | idna==3.2 25 | Jinja2==3.0.3 26 | kombu==5.2.3 27 | lockfile==0.12.2 28 | MarkupSafe==2.0.1 29 | mysqlclient==2.0.3 30 | packaging==21.3 31 | paramiko==2.8.0 32 | pexpect==4.8.0 33 | prettytable==2.5.0 34 | prompt-toolkit==3.0.28 35 | psutil==5.9.0 36 | ptyprocess==0.7.0 37 | pycparser==2.21 38 | pyecharts==1.9.1 39 | PyJWT==2.1.0 40 | PyNaCl==1.4.0 41 | pyparsing==3.0.7 42 | python-daemon==2.3.0 43 | pytz==2021.3 44 | PyYAML==6.0 45 | redis==4.1.4 46 | requests==2.26.0 47 | resolvelib==0.5.4 48 | simplejson==3.17.6 49 | six==1.16.0 50 | sqlparse==0.4.2 51 | urllib3==1.26.7 52 | uWSGI==2.0.20 53 | vine==5.0.0 54 | wcwidth==0.2.5 55 | websockets==10.2 56 | wrapt==1.13.3 57 | -------------------------------------------------------------------------------- /requirements.old: -------------------------------------------------------------------------------- 1 | amqp==5.0.9 2 | ansible==5.4.0 3 | ansible-core==2.12.3 4 | ansible-runner==2.1.2 5 | asgiref==3.4.1 6 | asyncio==3.4.3 7 | bcrypt==3.2.0 8 | billiard==3.6.4.0 9 | celery==5.2.3 10 | certifi==2021.5.30 11 | cffi==1.15.0 12 | charset-normalizer==2.0.6 13 | click==8.0.4 14 | click-didyoumean==0.3.0 15 | click-plugins==1.1.1 16 | click-repl==0.2.0 17 | cryptography==36.0.0 18 | Deprecated==1.2.13 19 | Django==3.2.7 20 | django-filter==21.1 21 | djangorestframework==3.12.4 22 | djangorestframework-simplejwt==4.8.0 23 | docutils==0.18.1 24 | idna==3.2 25 | Jinja2==3.0.3 26 | kombu==5.2.3 27 | lockfile==0.12.2 28 | MarkupSafe==2.0.1 29 | mysqlclient==2.0.3 30 | packaging==21.3 31 | paramiko==2.8.0 32 | pexpect==4.8.0 33 | prettytable==2.5.0 34 | prompt-toolkit==3.0.28 35 | psutil==5.9.0 36 | ptyprocess==0.7.0 37 | pycparser==2.21 38 | pyecharts==1.9.1 39 | PyJWT==2.1.0 40 | PyNaCl==1.4.0 41 | pyparsing==3.0.7 42 | python-daemon==2.3.0 43 | pytz==2021.3 44 | PyYAML==6.0 45 | redis==4.1.4 46 | requests==2.26.0 47 | resolvelib==0.5.4 48 | simplejson==3.17.6 49 | six==1.16.0 50 | sqlparse==0.4.2 51 | urllib3==1.26.7 52 | uWSGI==2.0.20 53 | vine==5.0.0 54 | wcwidth==0.2.5 55 | websockets==10.2 56 | wrapt==1.13.3 57 | -------------------------------------------------------------------------------- /service_conf/daemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "registry-mirrors": ["https://73yi6cz9.mirror.aliyuncs.com"] 3 | } -------------------------------------------------------------------------------- /service_conf/dnsmasq.conf: -------------------------------------------------------------------------------- 1 | # config dnsmasq 2 | 3 | # [dns] 4 | no-resolv 5 | server=223.5.5.5 6 | server=223.6.6.6 7 | domain=smartpxe.com 8 | cache-size=500 9 | 10 | # [hosts] 11 | address=/server.smartpxe.com/server_addr 12 | address=/www.smartpxe.com/server_addr 13 | 14 | # [log] 15 | log-queries 16 | log-facility=/var/log/dnsmasq.log 17 | log-async=20 18 | 19 | # [tftp] 20 | enable-tftp 21 | tftp-root=/tftpboot 22 | 23 | # [dhcp] 24 | dhcp-hostsdir=/etc/dnsmasq_client 25 | interface=eth0 26 | bind-interfaces 27 | listen-address=server_addr,127.0.0.1 28 | dhcp-range=range_start,range_end,range_mask,24h 29 | dhcp-lease-max=1000 30 | dhcp-authoritative 31 | dhcp-option=3,gateway 32 | dhcp-option=6,server_addr 33 | dhcp-option=66,server_addr 34 | # dhcp-option=42,10.10.100.2 35 | 36 | # [pxe path] 37 | dhcp-match=set:bios,option:client-arch,0 38 | dhcp-match=set:ipxe,175 39 | dhcp-boot=tag:!ipxe,tag:bios,undionly.kpxe 40 | dhcp-boot=tag:!ipxe,tag:!bios,ipxe.efi 41 | dhcp-boot=tag:ipxe,tag:bios,http://server_addr/boot-bios.html 42 | dhcp-boot=tag:ipxe,tag:!bios,http://server_addr/boot-uefi.html -------------------------------------------------------------------------------- /service_conf/pip.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | index-url = https://mirrors.aliyun.com/pypi/simple/ 3 | 4 | [install] 5 | trusted-host=mirrors.aliyun.com -------------------------------------------------------------------------------- /service_conf/server.smartpxe.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name server.smartpxe.com; 4 | client_max_body_size 102400M; 5 | client_body_buffer_size 8000M; 6 | client_body_timeout 120; 7 | 8 | # web / 9 | location / { 10 | root /var/www/html/; 11 | autoindex on; 12 | autoindex_exact_size off; 13 | autoindex_localtime on; 14 | charset 'utf-8'; 15 | # try_files / =404; 16 | } 17 | } -------------------------------------------------------------------------------- /service_conf/smartpxe.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SmartPXE App Server 3 | After=network.target 4 | 5 | [Service] 6 | User=root 7 | Environment=TERM=xterm-256color 8 | WorkingDirectory=/opt/SmartPXE/ 9 | ExecStart=/usr/local/bin/uwsgi --ini /opt/SmartPXE/service_conf/uwsgi.ini 10 | Restart=always 11 | KillSignal=SIGQUIT 12 | Type=forking 13 | 14 | [Install] 15 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /service_conf/sources.list: -------------------------------------------------------------------------------- 1 | deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse 2 | deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse 3 | deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse 4 | deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse 5 | deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse 6 | -------------------------------------------------------------------------------- /service_conf/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket=127.0.0.1:8000 3 | chdir=/opt/SmartPXE/ 4 | module=smartpxe.wsgi:application 5 | master=True 6 | processes=cpu_count 7 | pidfile=/tmp/SmartPXE-master.pid 8 | daemonize=/var/log/SmartPXE-uwsgi.log 9 | vacuum=True -------------------------------------------------------------------------------- /service_conf/www.smartpxe.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name www.smartpxe.com; 4 | client_max_body_size 102400M; 5 | client_body_buffer_size 8000M; 6 | client_body_timeout 120; 7 | 8 | # backend api 9 | location ^~ /api/v1/ { 10 | rewrite ^/api/v1(/.*) $1 break; 11 | # proxy_pass http://127.0.0.1:8000; 12 | uwsgi_pass 127.0.0.1:8000; 13 | include uwsgi_params; 14 | } 15 | 16 | # frontend 17 | location / { 18 | root /var/www/html/SmartPXE/; 19 | index index.html; 20 | charset 'utf-8'; 21 | # try_files / =404; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from glob import glob 3 | 4 | setuptools.setup( 5 | name="SmartPXE", 6 | version="0.6.0", 7 | author="lcp", 8 | author_mail="cplinux98@gmail.com", 9 | description="SmartPXE Devops Management", 10 | url="https://www.linux98.com", 11 | classifiers=[ 12 | "Programming Language :: Python :: 3", 13 | "License :: OSI Approved :: MIT License", 14 | "Operating System :: OS Independent", 15 | ], 16 | package_dir={"": "."}, 17 | packages=setuptools.find_packages(), 18 | python_requires=">=3.8.0", 19 | data_files=[ 20 | ('', ['requirements']), 21 | ('', glob('service_conf/**')), 22 | ('', glob('frontends/**/*', recursive=True)) 23 | ], 24 | py_modules=['manage'] 25 | ) 26 | -------------------------------------------------------------------------------- /smartpxe/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from .celery import app as celery_app 3 | 4 | __all__ = ('celery_app',) 5 | 6 | -------------------------------------------------------------------------------- /smartpxe/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for smartpxe project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartpxe.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /smartpxe/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | import os 3 | from celery import Celery 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartpxe.settings') 6 | 7 | app = Celery('smartpxe') 8 | 9 | app.config_from_object('django.conf:settings', namespace='CELERY') 10 | app.autodiscover_tasks() 11 | 12 | app.conf.broker_url = 'redis://127.0.0.1:6379/0' 13 | app.conf.broker_transport_options = {'visibility_timeout': 43200} 14 | app.conf.result_backend = 'redis://127.0.0.1:6379/1' 15 | 16 | app.conf.update( 17 | enable_utc = True, 18 | timezone = 'Asia/Shanghai' 19 | ) 20 | -------------------------------------------------------------------------------- /smartpxe/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for smartpxe project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | # 修改路径为拆分后的路径。 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-)o)a!k!m^)n)i$cue98udsr5^(64gd+^i0g52-s_krbf4q5dp9' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | # 由下面进行进行统一环境管理 28 | # DEBUG = True 29 | # 30 | # ALLOWED_HOSTS = ['*',] 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'install', 43 | 'temp', 44 | 'django_filters', 45 | 'dashboard', 46 | 'task', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | # 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'smartpxe.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'smartpxe.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 82 | 83 | # Password validation 84 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 85 | 86 | AUTH_PASSWORD_VALIDATORS = [ 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 89 | }, 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 98 | }, 99 | ] 100 | 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 104 | 105 | LANGUAGE_CODE = 'zh-Hans' # 'en-us' 106 | 107 | TIME_ZONE = 'Asia/Shanghai' # 'UTC' 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = False 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 118 | 119 | STATIC_URL = '/static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | 126 | # 控制当前settings配置状态 127 | # 0 => 开发环境, 1 => 生产环境 128 | CONF_STATUS = 1 129 | 130 | if CONF_STATUS: 131 | print('当前环境为: 生产环境', "+++++++++++++++++++++++++++++++++=") 132 | DEBUG = False 133 | ALLOWED_HOSTS = ['*',] 134 | # 生产环境数据库 135 | DATABASES = { 136 | 'default': { 137 | 'ENGINE': 'django.db.backends.mysql', 138 | 'NAME': 'smartpxe', 139 | 'USER': 'root', 140 | 'PASSWORD': '123456', 141 | 'HOST': '127.0.0.1', 142 | 'PORT': '3306', 143 | } 144 | } 145 | # drf配置 146 | REST_FRAMEWORK = { 147 | # 'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler', # 全局拦截器 148 | 'DEFAULT_AUTHENTICATION_CLASSES': ( # JWT token认证 149 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 150 | ), 151 | # 'DEFAULT_PERMISSION_CLASSES': [ # 设置权限策略, 152 | # 'rest_framework.permissions.IsAuthenticated', # 设置所有连接都要经过认证 153 | # 'utils.permissions.CrudModelPermission' 154 | # ], 155 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 156 | 'DEFAULT_PAGINATION_CLASS': 'utils.paginations.PageNumberPagination', 157 | 'PAGE_SIZE': 5, 158 | } 159 | 160 | # jwt 认证配置 161 | from datetime import timedelta 162 | SIMPLE_JWT = { 163 | 'ACCESS_TOKEN_LIFETIME': timedelta(hours=12), # jwt授权的token时效 164 | } 165 | 166 | # 程序基础路径配置 167 | # 程序基础路径配置 168 | IMAGES_UPLOADS_DIR = "/tmp" 169 | SERVER_URL = "http://server_addr" # server.smartpxe.com 170 | SERVER_API = "http://server_api" # www.smartpxe.com/api/v1 171 | HTTP_DIR = "/var/www/html/" 172 | KS_DIR = HTTP_DIR + "ks/" 173 | PUBLIC_KEY = "public_key" 174 | 175 | else: 176 | print('当前环境为: 开发环境', "+++++++++++++++++++++++++++++++++=") 177 | DEBUG = True 178 | ALLOWED_HOSTS = ['*',] 179 | # 数据库配置 180 | DATABASES = { 181 | 'default': { 182 | 'ENGINE': 'django.db.backends.mysql', 183 | 'NAME': 'smartpxe', 184 | 'USER': 'root', 185 | 'PASSWORD': '123456', 186 | 'HOST': '172.16.54.1', 187 | 'PORT': '3306', 188 | } 189 | } 190 | # 在Django的控制台中打印sql执行语句 191 | LOGGING = { 192 | 'version': 1, 193 | 'disable_existing_loggers': False, 194 | 'handlers': { 195 | 'console': { 196 | 'class': 'logging.StreamHandler', 197 | }, 198 | }, 199 | 'loggers': { 200 | 'django.db.backends': { 201 | 'handlers': ['console'], 202 | 'level': 'DEBUG', 203 | }, 204 | }, 205 | } 206 | # drf配置 207 | REST_FRAMEWORK = { 208 | # 'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler', # 全局拦截器 209 | # 'DEFAULT_AUTHENTICATION_CLASSES': ( # JWT token认证 210 | # 'rest_framework_simplejwt.authentication.JWTAuthentication', 211 | # ), 212 | # 'DEFAULT_PERMISSION_CLASSES': [ # 设置权限策略, 213 | # 'rest_framework.permissions.IsAuthenticated', # 设置所有连接都要经过认证 214 | # 'utils.permissions.CrudModelPermission' 215 | # ], 216 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 217 | 'DEFAULT_PAGINATION_CLASS': 'utils.paginations.PageNumberPagination', 218 | 'PAGE_SIZE': 10, 219 | } 220 | 221 | # jwt认证配置 222 | # from datetime import timedelta 223 | # SIMPLE_JWT = { 224 | # 'ACCESS_TOKEN_LIFETIME': timedelta(hours=12), # jwt授权的token时效 225 | # } 226 | 227 | # 程序基础路径配置 228 | IMAGES_UPLOADS_DIR = "/tmp" 229 | SERVER_URL = "http://10.10.100.2" 230 | SERVER_API = "http://10.10.100.2:8000" 231 | HTTP_DIR = "/var/www/html/" 232 | KS_DIR = HTTP_DIR + "ks/" 233 | PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3PxjQfN5/1WcEmkoTBnpKTsyJAZQd1lSYoEvtKv87V5prTWEOAhzxZ0P6X8T/xpQXk4o+F0FnsQIm2V5J32vzlsKCd5cEpf+laihBkkPyIpT8whvHC4uy2MCxM9PxT71Thde2o3fk2tIETX2lEcCF1jMMXg58Mv22YMB9kaw1AEXt2aPAnZv5GUwu4Rzk+0DC174vgXr6P7C79/dFPCwpqRSjREEvDusQSOEfE0zxlOS5SxBaRxiB93WWi9UUgQtBKoxhiaSaeJcBBW3EgkIexKCC7Zvnv2+T70/Kcs7EefrsWFKEmLEv/x5VjVCA5n1h4OSXjchw9tTBlYqbCSb082IndNdukbKheJax5fMku608CHl8DOLoZcVyzCFaxNtczpp7Xg/8ji5o29LllV0xqRYOkXZQrp8aG4mk0IfzIZsPfEmTFdDmfR+edLiyG6hK3GESatmYMEWanIAYdalr+HgJqlw9bVcoN97nh+v1J7nxaL4z3s3a2t5W/Cyk3C8= root@pxeserver" 234 | -------------------------------------------------------------------------------- /smartpxe/urls.py: -------------------------------------------------------------------------------- 1 | """smartpxe URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from django.urls import include 19 | from utils.healthy_check import check_healthy 20 | 21 | from rest_framework_simplejwt.views import ( 22 | TokenObtainPairView, 23 | TokenRefreshView, 24 | ) 25 | 26 | urlpatterns = [ 27 | path('login/', TokenObtainPairView.as_view(), name='login'), 28 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 29 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 30 | # path('admin/', admin.site.urls), 31 | path('install/', include('install.urls')), 32 | path('temp/', include('temp.urls')), 33 | path('dashboard/', include('dashboard.urls')), 34 | path('task/', include('task.urls')), 35 | path('healthz/', check_healthy) 36 | ] 37 | print('=' * 30) 38 | print(urlpatterns) 39 | print('=' * 30) 40 | -------------------------------------------------------------------------------- /smartpxe/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for smartpxe project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'smartpxe.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/task/__init__.py -------------------------------------------------------------------------------- /task/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /task/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TaskConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'task' 7 | -------------------------------------------------------------------------------- /task/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/task/migrations/__init__.py -------------------------------------------------------------------------------- /task/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # task 4 | # 从发现记录列表转到这里 5 | class TaskList(models.Model): 6 | isVM = models.BooleanField(verbose_name="是否为虚拟机") 7 | sn = models.CharField(blank=False, max_length=20, verbose_name="SN") 8 | mac = models.CharField(blank=False, max_length=20, verbose_name="MAC地址", primary_key=True) 9 | vender = models.CharField(blank=True, null=True, max_length=200, verbose_name='厂家') 10 | product = models.CharField(blank=True, null=True, max_length=200, verbose_name='产品') 11 | cpuinfo = models.CharField(blank=True, null=True, max_length=200, verbose_name='CPU') 12 | meminfo = models.CharField(blank=True, null=True, max_length=200, verbose_name='内存') 13 | status = models.BooleanField(default=True) 14 | clientip = models.GenericIPAddressField(blank=False, verbose_name="Client IP") 15 | ipmi = models.GenericIPAddressField(blank=True, null=True, verbose_name="IPMI IP") 16 | date = models.DateTimeField(blank=False, verbose_name="加入时间", auto_now=True) 17 | sysinfo = models.JSONField(verbose_name="系统信息") 18 | 19 | class Meta: 20 | verbose_name = '任务主机列表' 21 | db_table = 'p_task_list' 22 | ordering = ['date'] 23 | 24 | def __str__(self): 25 | return self.sn 26 | 27 | # playbook 28 | class PlaybookTemp(models.Model): 29 | # id 30 | name = models.CharField(blank=False, max_length=20, verbose_name="模板名称", unique=True) 31 | content = models.TextField() 32 | 33 | class Meta: 34 | verbose_name = '任务模板列表' 35 | db_table = 'p_temp_playbook' 36 | ordering = ['id'] 37 | 38 | def __str__(self): 39 | return self.name 40 | 41 | class TaskResult(models.Model): 42 | # id 43 | name = models.CharField(blank=False, max_length=20, verbose_name="主机名称") 44 | status = models.BooleanField() 45 | playbook = models.CharField(blank=True, max_length=20, verbose_name="执行模板") 46 | task_id = models.CharField(blank=True, max_length=200, verbose_name="任务执行ID") 47 | command = models.CharField(blank=True, max_length=120, verbose_name="执行命令") 48 | progress = models.IntegerField(blank=True, default=1, verbose_name="任务进度") 49 | date = models.DateTimeField(blank=False, verbose_name="结束时间", auto_now=True) 50 | result = models.TextField() 51 | 52 | class Meta: 53 | verbose_name = '任务结果列表' 54 | db_table = 'p_task_result' 55 | ordering = ['date'] 56 | 57 | def __str__(self): 58 | return self.name 59 | -------------------------------------------------------------------------------- /task/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import TaskList, TaskResult, PlaybookTemp 3 | 4 | 5 | class TaskListSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = TaskList 8 | fields = '__all__' 9 | 10 | 11 | class TaskResultSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = TaskResult 14 | fields = '__all__' 15 | 16 | 17 | class PlaybookTempSerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = PlaybookTemp 20 | fields = '__all__' 21 | 22 | -------------------------------------------------------------------------------- /task/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from smartpxe.celery import app 3 | import redis 4 | import ansible_runner 5 | import time 6 | import json 7 | from utils.tools import AddTaskRecord 8 | import os 9 | import uuid 10 | from pathlib import Path 11 | import shutil 12 | 13 | pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True, db=3) 14 | 15 | my_playbook = """ 16 | --- 17 | - name: test host 18 | hosts: all 19 | tasks: 20 | - name: shell id 21 | command: "ifconfig" 22 | """ 23 | from utils.tools import RunAnsible 24 | 25 | def template(host, status, message): 26 | data = { 27 | "host": host, 28 | "status": status, 29 | "message": message 30 | } 31 | return json.dumps(data) 32 | 33 | @app.task(name='run_playbook') 34 | def run_playbook(host, name, playbook, record_id): 35 | print('start') 36 | a = RunAnsible(host) 37 | t = AddTaskRecord(record_id) 38 | task_id = a.temp_id 39 | t.add(task_id) 40 | message = '任务已经开始运行' 41 | try: 42 | r = redis.Redis(connection_pool=pool) 43 | r.set(task_id, template(name, "start", message)) 44 | # events = 45 | for event in a.run_playbook(playbook): 46 | message = message + "\r\n" + event['stdout'] 47 | r.set(task_id, template(name, "running", message)) 48 | r.set(task_id, template(name, "end", message)) 49 | time.sleep(5) 50 | print(a.clear()) 51 | message = message + "\r\n任务已完成\r\n" 52 | r.psetex(task_id, 5000, message) 53 | # add redis value to mysql 54 | t.done(message) 55 | except Exception as e: 56 | print(e) 57 | finally: 58 | r.close() 59 | print('end') 60 | 61 | 62 | 63 | # def make_inventory(host): 64 | # data = { 65 | # "all": { 66 | # "hosts": host 67 | # } 68 | # } 69 | # return data 70 | # 71 | # def write_playbook(content): 72 | # temp_id = str(uuid.uuid4()).replace("-", '') 73 | # data_dir = os.path.join("/opt", "ansible", temp_id) 74 | # with open(playbook_path, 'w+') as fd: 75 | # fd.write(my_playbook) 76 | 77 | # materials = Materials("10.10.100.39", my_playbook) 78 | # m = ansible_runner.run( 79 | # private_data_dir=materials.data_dir, 80 | # inventory=materials.inventory(), 81 | # playbook=materials.playbook(), 82 | # quiet=True 83 | # ) 84 | # events = m[1].events 85 | 86 | 87 | # class Materials: 88 | # """ 89 | # mm = Metarials(host='10.10.100.39', content="") 90 | # mm.data_dir => "/opt/ansible/xxx/" 91 | # mm.inventory => {"all":{"hosts": "10.10.100.39"}} 92 | # mm.playbook => "/opt/ansible/xxx/playbook01.yaml" 93 | # mm.clear => rm -rf data_dir 94 | # """ 95 | # def __init__(self, host, content): 96 | # self.host = host 97 | # self.content = content 98 | # self.temp_id = str(uuid.uuid4()).replace("-", '') 99 | # self.data_dir = os.path.join("/opt", "ansible", self.temp_id) 100 | # mk_data_dir = Path(self.data_dir).mkdir(parents=True, exist_ok=True) 101 | # 102 | # def inventory(self): 103 | # data = { 104 | # "all": { 105 | # "hosts": self.host 106 | # } 107 | # } 108 | # return data 109 | # 110 | # def playbook(self): 111 | # playbook_path = os.path.join(self.data_dir, 'playbook.yaml') 112 | # with open(playbook_path, 'w+') as fd: 113 | # fd.write(self.content) 114 | # return playbook_path 115 | # 116 | # def clear(self): 117 | # if Path(self.data_dir).exists(): 118 | # shutil.rmtree(self.data_dir, ignore_errors=True) 119 | # return 1 -------------------------------------------------------------------------------- /task/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /task/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from task.views import TaskListViewSet, TaskResultSerializerViewSet, PlaybookTempSerializerViewSet 4 | from rest_framework.routers import SimpleRouter 5 | 6 | router = SimpleRouter() 7 | router.register('hostlist', TaskListViewSet) 8 | router.register('template', PlaybookTempSerializerViewSet) 9 | router.register('result', TaskResultSerializerViewSet) 10 | 11 | urlpatterns = [ 12 | ] + router.urls 13 | 14 | 15 | print('=' * 30) 16 | print(urlpatterns) 17 | print('=' * 30) 18 | -------------------------------------------------------------------------------- /task/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render 4 | from rest_framework.views import Response, Request 5 | from rest_framework.viewsets import ModelViewSet 6 | from .serializers import TaskListSerializer, TaskResultSerializer, PlaybookTempSerializer 7 | from .models import TaskList, TaskResult, PlaybookTemp 8 | from rest_framework.decorators import action 9 | from utils.tools import RunAnsible 10 | from django.db import transaction 11 | from install.serializers import InstallPreListSerializer 12 | import redis 13 | # Create your views here. 14 | 15 | 16 | 17 | class TaskListViewSet(ModelViewSet): 18 | queryset = TaskList.objects.all() 19 | serializer_class = TaskListSerializer 20 | 21 | @action(methods=['POST'], detail=True, url_path='convert') 22 | def convert_to_install(self, request:Request, pk): 23 | # 这个是转换的api,发送 要转换的pk 和 要转换到install_pre 24 | with transaction.atomic(): 25 | obj = self.get_object() 26 | serializer = TaskListSerializer(obj) 27 | data = serializer.data 28 | toserializer = InstallPreListSerializer(data=data) 29 | validated = toserializer.is_valid(raise_exception=True) 30 | toserializer.save() 31 | obj.delete() 32 | return Response(status=201) 33 | 34 | @action(methods=['POST'], detail=True, url_path='command') 35 | def run_command(self, request: Request, pk): 36 | print(request.data) 37 | model = request.data['model'] 38 | model_args = request.data['args'] 39 | obj = self.get_object() 40 | serializer = self.get_serializer(obj) 41 | data = serializer.data 42 | ipaddr = data['clientip'] 43 | # ipaddr = "10.10.100.39" 44 | ret = RunAnsible(ipaddr).run_model(model, model_args) 45 | # ret = _ret.replace('\n', '
') 46 | return Response(ret) 47 | 48 | @action(methods=['POST'], detail=True, url_path='playbook') 49 | def run_playbook(self, request: Request, pk): 50 | obj = self.get_object() 51 | serializer = self.get_serializer(obj) 52 | data = serializer.data 53 | ipaddr = data['clientip'] 54 | sn = data['sn'] 55 | playbook_id = request.data['tempid'] 56 | get_playbook_obj = PlaybookTemp.objects.filter(pk=playbook_id).values('name', 'content') 57 | # print(PlaybookTemp.objects.filter(pk=playbook_id)[0]) 58 | playbook = get_playbook_obj[0].get('content') 59 | playbook_name = get_playbook_obj[0].get('name') 60 | from .tasks import run_playbook 61 | try: 62 | toserializer = TaskResultSerializer(data={ 63 | "name": sn, 64 | "status": 1, 65 | "playbook": playbook_name, 66 | "result": "任务提交完成" 67 | }) 68 | validated = toserializer.is_valid(raise_exception=True) 69 | toserializer.save() 70 | record_id = toserializer.data['id'] 71 | run_playbook.delay(host=ipaddr, name=sn, playbook=playbook, record_id=record_id) 72 | 73 | except Exception as e: 74 | print(e) 75 | return Response('任务提交失败') 76 | return Response('任务已经提交') 77 | 78 | 79 | 80 | 81 | 82 | class TaskResultSerializerViewSet(ModelViewSet): 83 | queryset = TaskResult.objects.all() 84 | serializer_class = TaskResultSerializer 85 | 86 | @action(methods=['GET'], detail=False, url_path='running') 87 | def get_running(self, request: Request): 88 | try: 89 | task_id = request.query_params.get('taskid') 90 | print(task_id) 91 | pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True, db=3) 92 | r = redis.Redis(connection_pool=pool) 93 | rdata = r.get(task_id) 94 | if not rdata: 95 | return Response({'code': 888, 'message': '任务执行已结束!'}) 96 | ret = json.loads(rdata).get('message') 97 | except Exception as e: 98 | return Response({'code': 888, 'message': '任务执行已结束!'}) 99 | # return Response({'code': 888, 'message': str(e)}) 100 | # r.close() 101 | return Response(ret) 102 | 103 | class PlaybookTempSerializerViewSet(ModelViewSet): 104 | queryset = PlaybookTemp.objects.all() 105 | serializer_class = PlaybookTempSerializer 106 | -------------------------------------------------------------------------------- /temp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/temp/__init__.py -------------------------------------------------------------------------------- /temp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /temp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TempConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'temp' 7 | -------------------------------------------------------------------------------- /temp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/temp/migrations/__init__.py -------------------------------------------------------------------------------- /temp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # 系统镜像 5 | class ImageTemp(models.Model): 6 | OS_TYPE_CHOICES = [ 7 | ('CentOS', 'CentOS'), 8 | ('Ubuntu', 'Ubuntu'), 9 | ('RHEL', 'RHEL'), 10 | ('Windows', 'Windows'), 11 | ] 12 | name = models.CharField(blank=False, max_length=48, verbose_name="镜像名称", unique=True) 13 | type = models.CharField(blank=False, choices=OS_TYPE_CHOICES, max_length=48, verbose_name="系统类型") 14 | version = models.CharField(blank=False, max_length=48, verbose_name="系统版本") 15 | path = models.CharField(blank=False, max_length=250, verbose_name="存储路径") 16 | save_path = models.CharField(blank=False, max_length=250, verbose_name="实际存储位置") 17 | 18 | class Meta: 19 | verbose_name = '系统镜像' 20 | db_table = 'p_temp_image' 21 | ordering = ['id'] 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | 27 | # 系统模板ks 28 | class OSTemp(models.Model): 29 | # id 30 | name = models.CharField(blank=False, max_length=20, verbose_name="模板名称", unique=True) 31 | image = models.ForeignKey(ImageTemp, models.PROTECT, db_column='image_name', to_field='name') 32 | config = models.TextField() 33 | path = models.CharField(blank=True, max_length=250, verbose_name="存储路径") 34 | save_path = models.CharField(blank=True, max_length=250, verbose_name="实际存储位置") 35 | 36 | class Meta: 37 | verbose_name = '系统模板' 38 | db_table = 'p_temp_os_config' 39 | ordering = ['id'] 40 | 41 | def __str__(self): 42 | return self.name 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /temp/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import ImageTemp, OSTemp 3 | 4 | 5 | class ImageTempSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = ImageTemp 8 | fields = '__all__' 9 | 10 | 11 | class OSTempSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = OSTemp 14 | fields = '__all__' 15 | 16 | -------------------------------------------------------------------------------- /temp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /temp/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from rest_framework.routers import SimpleRouter 4 | from .views import ImageTempViewSet, upload, OSTempViewSet, publickey, extract_image 5 | 6 | router = SimpleRouter() 7 | router.register('image', ImageTempViewSet) 8 | router.register('config', OSTempViewSet) 9 | 10 | urlpatterns = [ 11 | # path('upload/', upload), 12 | path('extract/', extract_image), 13 | path('publickey/', publickey) 14 | ] + router.urls 15 | 16 | 17 | print('=' * 30) 18 | print(urlpatterns) 19 | print('=' * 30) 20 | -------------------------------------------------------------------------------- /temp/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render 4 | 5 | # Create your views here. 6 | from rest_framework.permissions import IsAuthenticated, IsAdminUser 7 | from rest_framework.viewsets import ModelViewSet 8 | from rest_framework.request import Request 9 | from rest_framework.response import Response 10 | from .models import ImageTemp, OSTemp 11 | from .serializers import ImageTempSerializer, OSTempSerializer 12 | from rest_framework.decorators import api_view, action, permission_classes 13 | from django.core.files.uploadedfile import InMemoryUploadedFile 14 | from install.serializers import InstallPreListSerializer 15 | from pathlib import Path 16 | from django.conf import settings 17 | from datetime import datetime 18 | from uuid import uuid4 19 | import os 20 | import shutil 21 | from utils.tools import replace_ks_url 22 | from rest_framework import status 23 | from rest_framework.response import Response 24 | from django.db import transaction 25 | from rest_framework.settings import api_settings 26 | 27 | 28 | class ImageTempViewSet(ModelViewSet): 29 | queryset = ImageTemp.objects.all() 30 | serializer_class = ImageTempSerializer 31 | 32 | def destroy(self, request, *args, **kwargs): 33 | # 删除镜像时,先判断镜像是否存在,如果存在则删除 34 | instance = self.get_object() 35 | serializer = self.get_serializer(instance) 36 | file_path = Path(serializer.data['save_path']) 37 | if file_path and Path.exists(file_path): 38 | try: 39 | instance.delete() 40 | shutil.rmtree(file_path, ignore_errors=True) 41 | return Response(status=204) 42 | except: 43 | return Response({'code': 888, 'message': '镜像删除失败,请检查关联系统模板是否删除!'}) 44 | else: 45 | return Response({'code': 888, 'message': '镜像删除失败,镜像在系统中不存在!'}) 46 | 47 | 48 | class OSTempViewSet(ModelViewSet): 49 | queryset = OSTemp.objects.all() 50 | serializer_class = OSTempSerializer 51 | 52 | @classmethod 53 | def write_file(cls, save_path, config): 54 | Path(os.path.dirname(save_path)).mkdir(parents=True, exist_ok=True) 55 | with open(save_path, 'wb+') as f: 56 | f.write(config) 57 | return 1 58 | 59 | def create(self, request, *args, **kwargs): 60 | """ 1. save and check""" 61 | with transaction.atomic(): 62 | image_name = request.data["image"] 63 | ks_name = request.data["name"] 64 | ks_content = request.data["config"].encode("UTF-8") 65 | path = '{}/ks/{}/{}.ks'.format(settings.SERVER_URL, image_name, ks_name) 66 | save_path = os.path.join(settings.KS_DIR, image_name, "{}.ks".format(ks_name)) 67 | request.data["path"] = path 68 | request.data["save_path"] = save_path 69 | ret = super().create(request, *args, **kwargs) 70 | self.write_file(save_path, ks_content) 71 | return ret 72 | 73 | def destroy(self, request, *args, **kwargs): 74 | # 删除记录时应该判断一下KS文件是否存在,如果存在应该将KS文件一起删除 75 | instance = self.get_object() 76 | serializer = self.get_serializer(instance) 77 | ks_path = serializer.data['save_path'] 78 | print(ks_path) 79 | self.perform_destroy(instance) 80 | if ks_path and Path(ks_path).exists(): 81 | os.remove(Path(ks_path)) 82 | return Response(status=204) 83 | 84 | def update(self, request, *args, **kwargs): 85 | instance = self.get_object() 86 | serializer = self.get_serializer(instance, data=request.data) 87 | serializer.is_valid(raise_exception=True) 88 | if serializer.save(): 89 | file_name = serializer.data['save_path'] 90 | config = serializer.data['config'].encode("utf-8") 91 | with open(Path(file_name), 'wb+') as fd: 92 | fd.write(config) 93 | 94 | return Response(serializer.data) 95 | 96 | @action(['post'], detail=False, url_path="generate") 97 | def generate_ks(self, request, *args, **kwargs): 98 | obj = ImageTemp.objects.all().filter(name__exact=request.data['image']) 99 | image_info = ImageTempSerializer(obj, many=True).data[0] 100 | print(image_info, type(image_info)) 101 | image_path = image_info['path'] 102 | old_config = request.data['config'] 103 | new_config = replace_ks_url(old_config, image_path, settings.SERVER_API) 104 | # 记得替换 105 | code = new_config[0] 106 | message = new_config[1] 107 | if code: 108 | return Response(message) 109 | else: 110 | return Response({'code': 888, 'message': message}) 111 | 112 | # @permission_classes([IsAuthenticated]) 113 | @api_view(['POST']) 114 | def extract_image(request: Request): 115 | data = request.data 116 | image_path = data['path'] 117 | image_name = data['name'] 118 | image_type = data['type'] 119 | image_version = data['version'] 120 | if not Path(image_path).exists(): 121 | return Response({'code': 888, 'message': '镜像不存在'}) 122 | if Path(image_path).suffix != '.iso': 123 | return Response({'code': 888, 'message': '请检查镜像格式'}) 124 | os.popen("mount -o loop {} /media".format(image_path)).read() 125 | save_path = Path('/var/www/html/images/{}/{}/{}/'.format(image_type, image_version, image_name)) 126 | save_path.mkdir(parents=True, exist_ok=True) 127 | os.popen("rsync -a /media/ {}".format(save_path)).read() 128 | os.popen("umount /media").read() 129 | dest_url = '{}/images/{}/{}/{}'.format(settings.SERVER_URL, image_type, image_version, image_name) 130 | 131 | return Response({'name': image_name, 'url': dest_url, 'save_path': str(save_path)}) 132 | 133 | # @permission_classes([IsAuthenticated]) 134 | @api_view(['POST']) 135 | def upload(request: Request): 136 | file_fields_name = 'file' 137 | file_obj: InMemoryUploadedFile = request.data.get(file_fields_name) 138 | print(file_obj.name) 139 | print(request.data) 140 | print('~' * 30) 141 | uploads_dir = Path(settings.IMAGES_UPLOADS_DIR) 142 | parent_dir = Path("{:%Y/%m/%d}".format(datetime.now())) 143 | (uploads_dir / parent_dir).mkdir(parents=True, exist_ok=True) 144 | filename = Path(uuid4().hex + '.iso') 145 | temp_path = uploads_dir / parent_dir / filename 146 | with open(temp_path, 'wb') as f: 147 | for c in file_obj.chunks(): 148 | f.write(c) 149 | print(temp_path) 150 | 151 | os.popen("mount -o loop {} /media".format(temp_path)).read() 152 | type = request.data['type'] 153 | version = request.data['version'] 154 | name = request.data['name'] 155 | save_path = Path('/var/www/html/images/{}/{}/{}/'.format(type, version, name)) 156 | save_path.mkdir(parents=True, exist_ok=True) 157 | os.popen("rsync -a /media/ {}".format(save_path)).read() 158 | os.popen("umount /media").read() 159 | if Path.exists(temp_path): 160 | os.remove(temp_path) 161 | 162 | dest_url = '{}/images/{}/{}/{}'.format(settings.SERVER_URL, type, version, name ) 163 | 164 | return Response({'name': file_obj.name, 'url': dest_url, 'save_path': str(save_path)}) 165 | 166 | 167 | 168 | # @permission_classes([]) 169 | @api_view(['GET']) 170 | def publickey(request): 171 | key = settings.PUBLIC_KEY.replace('\"', '') 172 | return Response(key) 173 | 174 | 175 | 176 | 177 | 178 | # @api_view(['POST']) 179 | # def generate_standard_config(request: Request): 180 | # """ 181 | # :param: config , image , name 182 | # :return: file_url 183 | # 根据发送过来的ks配置信息,镜像id, name,去生成标准的ks配置信息 184 | # """ 185 | # 186 | # obj = ImageTemp.objects.all().filter(name__exact=request.data['image']) 187 | # image_info = ImageTempSerializer(obj, many=True).data[0] 188 | # print(image_info, type(image_info)) 189 | # image_path = image_info['path'] 190 | # image_name = image_info['name'] 191 | # file_name = request.data['name'] 192 | # old_config = request.data['config'] 193 | # new_config = replace_ks_url(old_config, image_path, settings.SERVER_API) 194 | # # 记得替换 195 | # print(new_config) 196 | # code, message = new_config[0], new_config[1] 197 | # if code: 198 | # return Response(message) 199 | # return Response({"code": code, "message": message}) 200 | # 201 | 202 | 203 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cplinux98/SmartPXE/aa4315fc9aa0877a34ed9aa7a9b4ba8ee8e3c442/utils/__init__.py -------------------------------------------------------------------------------- /utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from rest_framework import exceptions 3 | from django.core.exceptions import PermissionDenied 4 | from rest_framework.views import set_rollback, Response 5 | 6 | 7 | class MagBaseException(exceptions.APIException): 8 | code = "10000" # 非0都是异常 9 | message = "非法请求" 10 | 11 | @classmethod 12 | def get_msg(cls): 13 | return {'code': cls.code, 'message': cls.message} 14 | 15 | 16 | class InvalidToken(MagBaseException): # 405 => 400 17 | code = 1 18 | message = "认证已过期,请重新登陆!" 19 | 20 | 21 | class AuthenticationFailed(MagBaseException): 22 | code = 2 23 | message = "用户名或密码无效!" 24 | 25 | 26 | class NotAuthenticated(MagBaseException): 27 | code = 3 28 | message = "未登录,请登陆后重试!" 29 | 30 | 31 | class ValidationError(MagBaseException): 32 | code = 101 33 | message = "后端验证失败,请检查输入信息!" 34 | 35 | class InvalidPassword(MagBaseException): 36 | code = 102 37 | message = "密码修改失败,请重试!" 38 | 39 | 40 | # 映射 401 => 400; 403 => 400 41 | exc_map = { 42 | "InvalidToken": InvalidToken, # token过期 43 | "AuthenticationFailed": AuthenticationFailed, # 密码无效 44 | "NotAuthenticated": NotAuthenticated, # 无token登陆 45 | "ValidationError": ValidationError, # 后端验证失败 46 | "InvalidPassword": InvalidPassword 47 | } 48 | 49 | 50 | def exception_handler(exc, context): 51 | """自己的全局异常处理""" 52 | if isinstance(exc, Http404): 53 | exc = exceptions.NotFound() 54 | elif isinstance(exc, PermissionDenied): 55 | exc = exceptions.PermissionDenied() 56 | 57 | print('~' * 30) 58 | print(type(exc), exc) 59 | print('~' * 30) 60 | 61 | if isinstance(exc, exceptions.APIException): 62 | set_rollback() 63 | data = exc_map.get(exc.__class__.__name__, MagBaseException).get_msg() 64 | return Response(data, status=200) 65 | 66 | return None 67 | -------------------------------------------------------------------------------- /utils/healthy_check.py: -------------------------------------------------------------------------------- 1 | from django.db import connections 2 | from django.db.utils import OperationalError 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | from django.http import HttpRequest 6 | 7 | 8 | def check_mysql(db_name='default'): 9 | db_conn = connections[db_name] 10 | try: 11 | check = db_conn.cursor() 12 | except OperationalError: 13 | return 0 14 | else: 15 | return 1 16 | 17 | 18 | def check_healthy(request: HttpRequest): 19 | if check_mysql(): 20 | return HttpResponse('ok') 21 | else: 22 | return HttpResponse('error') 23 | -------------------------------------------------------------------------------- /utils/paginations.py: -------------------------------------------------------------------------------- 1 | from rest_framework import pagination 2 | from rest_framework.response import Response 3 | 4 | 5 | class PageNumberPagination(pagination.PageNumberPagination): 6 | def get_paginated_response(self, data): 7 | return Response({ 8 | 'pagination': { 9 | 'total': self.page.paginator.count, 10 | 'size': self.page_size, 11 | 'page': self.page.number 12 | }, 13 | 'results': data, 14 | }) 15 | -------------------------------------------------------------------------------- /utils/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import DjangoModelPermissions 2 | 3 | 4 | class CrudModelPermission(DjangoModelPermissions): 5 | perms_map = { 6 | 'GET': ['%(app_label)s.add_%(model_name)s'], 7 | 'OPTIONS': ['%(app_label)s.add_%(model_name)s'], 8 | 'HEAD': ['%(app_label)s.add_%(model_name)s'], 9 | 'POST': ['%(app_label)s.add_%(model_name)s'], 10 | 'PUT': ['%(app_label)s.change_%(model_name)s'], 11 | 'PATCH': ['%(app_label)s.change_%(model_name)s'], 12 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'], 13 | } 14 | -------------------------------------------------------------------------------- /utils/tools.py: -------------------------------------------------------------------------------- 1 | # 要根据mac地址、系统版本、ks文件,去生成pxe菜单 2 | import pathlib 3 | import os 4 | import re 5 | import paramiko 6 | import subprocess 7 | import shutil 8 | import uuid 9 | import ansible_runner 10 | from task.models import TaskResult 11 | from task.serializers import TaskResultSerializer 12 | 13 | 14 | # pxe菜单模板 15 | text = """DEFAULT menu.c32 16 | MENU TITLE Welcome to Custom PXE Server 17 | PROMPT 0 18 | TIMEOUT 30 19 | 20 | DEFAULT Install 21 | 22 | LABEL Install 23 | MENU LABEL ^Install 24 | MENU DEFAULT 25 | KERNEL {} 26 | INITRD {} 27 | APPEND ks={} {} 28 | IPAPPEND 2 29 | """ 30 | 31 | 32 | 33 | 34 | # 根据mac地址、系统版本、ks文件,去生成pxe菜单 35 | def generate_pxe_menu(mac, kernel_path, initrd_path, ks_path, option=''): 36 | path = '/var/www/html/pxelinux.cfg/' 37 | mac = mac.replace(':', '-', 5) 38 | filename = '{}{}-{}'.format(path, '01', mac) 39 | print(filename) 40 | option = "inst.sshd net.ifnames=0 biosdevname=0" # vnc vncpassword=123456 41 | with open(filename, 'w') as f: 42 | f.write(text.format(kernel_path, initrd_path, ks_path, option)) 43 | return filename 44 | 45 | 46 | # 根据ks文本、repo_ur、server_url 去生成符合该服务标准的ks文本内容 47 | def replace_ks_url(old, url, server_url): 48 | try: 49 | new_repo_url = 'url --url=' + url 50 | new_server_url = 'server_url="' + server_url + '/install/progress/' + '${PXE_MAC}/"' 51 | regex_1 = re.compile('url --url=.*') 52 | regex_2 = re.compile('server_url=.*') 53 | _rest = old.replace(regex_1.findall(old)[0], new_repo_url) 54 | rest = _rest.replace(regex_2.findall(_rest)[0], new_server_url) 55 | except Exception as e: 56 | return 0, "ks文件内容不符合规范! \nerror : {}".format(e) 57 | return 1, rest 58 | 59 | # print(replace_ks_url(long_text, 'http://xxx.xxx.xxx', 'http://sddsd.sdds.com')) 60 | 61 | # 给BootOS发送安装指令 62 | def send_run_rom_scripts(host, command, username="root", password="root"): 63 | if command == "install": 64 | cmd = '/usr/bin/python3 /opt/install.py' 65 | elif command == "shutdown": 66 | cmd = 'poweroff' 67 | elif command == "reboot": 68 | cmd = 'reboot' 69 | else: 70 | cmd = '' 71 | try: 72 | # username = 'root' 73 | # password = 'root' 74 | client = paramiko.SSHClient() 75 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy) 76 | client.connect(host, 22, username, password) 77 | # 执行脚本 78 | stdin, stdout, stderr = client.exec_command(cmd) 79 | print(stdout.read().decode()) 80 | print(stderr.read().decode()) 81 | except Exception as e: 82 | print(e) 83 | 84 | 85 | class ManagerDnsmasq: 86 | """ 87 | /etc/dnsmasq_client/{mac}.conf 88 | {mac}, {ip} 89 | """ 90 | def __init__(self): 91 | self.host_dir = pathlib.Path("/etc/dnsmasq_client/") 92 | self.config = "/etc/dnsmasq.conf" 93 | 94 | def add(self, mac, ip): 95 | filename = pathlib.Path(self.host_dir / "{}.conf".format(mac.replace(':', '', 5))) 96 | with open(filename, 'w') as fd: 97 | fd.write("{}, {}".format(mac, ip)) 98 | 99 | def delete(self, mac): 100 | filename = pathlib.Path(self.host_dir / "{}.conf".format(mac.replace(':', '', 5))) 101 | print(filename) 102 | if pathlib.Path.exists(filename): 103 | os.remove(filename) 104 | else: 105 | return "file is not exists" 106 | 107 | 108 | class RunAnsible: 109 | def __init__(self, host): 110 | self.host = host 111 | self.temp_id = str(uuid.uuid4()).replace("-", '') 112 | self.data_dir = os.path.join("/opt", "ansible", self.temp_id) 113 | self.inventory = self.make_inventory(self.host) 114 | self.make_dir = pathlib.Path.mkdir(pathlib.Path(self.data_dir), parents=True, exist_ok=True) 115 | 116 | @classmethod 117 | def make_inventory(cls, host): 118 | data = { 119 | "all": { 120 | "hosts": host 121 | } 122 | } 123 | return data 124 | 125 | def clear(self): 126 | ret = pathlib.Path(self.data_dir).exists() 127 | if ret: 128 | ret2 = shutil.rmtree( 129 | self.data_dir, 130 | # ignore_errors=True 131 | ) 132 | return ('ok', ret2) 133 | else: 134 | return ('not', ret) 135 | 136 | def run_model(self, model=None, model_args=None): 137 | print(self.data_dir) 138 | m = ansible_runner.run( 139 | private_data_dir=self.data_dir, 140 | inventory=self.inventory, 141 | host_pattern="all", 142 | module=model, 143 | module_args=model_args, 144 | quiet=True 145 | ) 146 | print(m.rc) 147 | stdout = m.stdout.read() 148 | # stderr = m.stderr.read() 149 | self.clear() 150 | # if m.rc != 0: 151 | # print('error') 152 | # return stderr 153 | return stdout 154 | 155 | def run_playbook(self, playbook=None): 156 | playbook_path = os.path.join(self.data_dir, 'playbook.yaml') 157 | print(playbook_path) 158 | with open(playbook_path, 'w+') as fd: 159 | fd.write(playbook) 160 | 161 | m = ansible_runner.run_async( 162 | private_data_dir=self.data_dir, 163 | inventory=self.inventory, 164 | playbook=playbook_path, 165 | quiet=True 166 | ) 167 | events = m[1].events 168 | # stderr = m.stderr.read() 169 | return events 170 | 171 | 172 | class AddTaskRecord: 173 | """ 174 | add => add task start to mysql 175 | done => add task end to mysql 176 | """ 177 | def __init__(self, pk): 178 | self.pk = pk 179 | 180 | def add(self, task_id): 181 | data = { 182 | "task_id": task_id, 183 | "progress": 2, 184 | } 185 | instance = TaskResult.objects.filter(pk=self.pk)[0] 186 | serializer = TaskResultSerializer(instance, data=data, partial=True) 187 | serializer.is_valid(raise_exception=True) 188 | serializer.save() 189 | 190 | def done(self, result): 191 | data = { 192 | "status": 0, 193 | "progress": 3, 194 | "result": result 195 | } 196 | instance = TaskResult.objects.filter(pk=self.pk)[0] 197 | serializer = TaskResultSerializer(instance, data=data, partial=True) 198 | serializer.is_valid(raise_exception=True) 199 | serializer.save() 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | # class ManagerDHCP: 208 | # def __init__(self, host_name): 209 | # self.conf_file = "/etc/dhcp/dhcpd.conf" 210 | # self.conf = self.load_conf() 211 | # self.host_name = host_name 212 | # self.pattern = re.compile(r'host {} {{\n.*\n.*\n}}\n'.format(self.host_name), re.M|re.I) 213 | # self.template = """host {SN} {{\n hardware ethernet {MAC};\n fixed-address {IP};\n}}\n""" 214 | # 215 | # def load_conf(self): 216 | # with open(self.conf_file, 'r') as f: 217 | # ret = f.read() 218 | # return ret 219 | # 220 | # def write_conf(self): 221 | # with open(self.conf_file, 'w') as f: 222 | # f.write(self.conf) 223 | # 224 | # def search(self): 225 | # searchOBJ = re.search(self.pattern, self.conf) 226 | # if searchOBJ: 227 | # return 1 228 | # else: 229 | # return 0 230 | # 231 | # def delete(self, restart=1): 232 | # if self.search(): 233 | # self.conf = re.sub(self.pattern, '', self.conf) 234 | # # print(self.conf) 235 | # if restart == 1: 236 | # return self.restart_service() 237 | # else: 238 | # return 0 239 | # 240 | # def add(self, mac, ip): 241 | # self.delete(0) 242 | # self.conf = self.conf + self.template.format(SN=self.host_name, MAC=mac, IP=ip) 243 | # print(self.conf) 244 | # code = self.restart_service() 245 | # if code == 0: 246 | # return code 247 | # return code 248 | # 249 | # def restart_service(self): 250 | # # write self.old_conf to /etc/dhcp/dhcpd.conf 251 | # # if systemctl restart dhcpd ok, return 1, else return 0 252 | # self.write_conf() 253 | # code = 0 254 | # code = subprocess.call("dhcpd -t -q", shell=True) 255 | # if code != 0: 256 | # return 1 257 | # code = subprocess.call("service dhcpd restart", shell=True) 258 | # if code != 0: 259 | # return 1 260 | # return code --------------------------------------------------------------------------------