├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
4 |
5 |
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
--------------------------------------------------------------------------------