☰
基于Python+Django的自动化运维平台实战:从脚本到可审计系统
2026/10/7 3:59:12 网站建设 项目流程

简介:这是一套面向计算机专业毕业设计场景的自动化运维平台源码,基于Python与Django构建,适合正在准备毕设、需要完整Web项目参考的学生,以及想入门运维自动化的开发者。平台围绕服务器监控、日志分析、任务调度、配置管理、权限控制与RESTful API等模块展开,将Python的paramiko、psutil等运维能力与Django的MVT架构结合,形成可运行的集中管理工具。压缩包共1132个文件,约13.78MB,其中191个py文件承载后端逻辑,223个html与144个css、184个js构成前端界面,另有png、ttf、woff等静态资源及sh、yml、conf等部署配置,目录结构完整。目前已有312人学习下载。读者可据此理解Django项目分层组织方式,参考监控指标采集、定时任务与权限划分的实现思路,并借助现有代码快速搭建自己的毕设原型或二次开发基础。

1. 从一台跳板机到一套平台:自动化运维到底在自动什么

很多团队所谓的“自动化运维”,本质是几台跳板机上散落着几十个 shell 脚本,谁离职谁带走一批,出故障时没人敢动。基于 Python + Django 的自动化运维平台,要解决的就是把这些脚本、定时任务、主机资产、执行记录收进一个 Web 系统里,让运维动作可编排、可审计、可回滚。它适合两类人:一是手里已经有一堆重复操作(批量发版、日志清理、服务重启)的中小团队运维;二是想用 Django 把 Python 运维脚本产品化的后端开发。这一章先把边界划清楚:平台不是万能的,它替代的是“人肉登录机器敲命令”,不是替代监控和 CI。选 Django 而不是 Flask/FastAPI,核心原因是它自带 ORM、Admin、认证和权限体系,运维平台最重的资产管理和任务记录这两块,Django 能省掉大量脚手架代码。

2. 平台骨架怎么搭:Django 项目结构与运维域模型

2.1 为什么运维平台的数据模型比普通 Web 应用更关键

普通 Web 应用的核心是用户和内容,运维平台的核心是“主机—任务—执行结果”这条链路。模型设计错了,后面任务编排会越写越乱。我一般会先落四张核心表:主机资产表(Host)、任务模板表(TaskTemplate)、执行记录表(TaskExecution)、执行明细表(TaskDetail)。主机表存 IP、SSH 端口、认证方式、所属业务组;任务模板存脚本内容或命令、参数定义、超时时间;执行记录存一次批量任务的总体状态;执行明细存每台机器的 stdout、stderr、退出码。这样设计的好处是,一次批量执行对应一条 TaskExecution 和 N 条 TaskDetail,查询“某台机器最近失败过哪些任务”只需要 join 一次。

# ops/models.py from django.db import models class Host(models.Model): ip = models.GenericIPAddressField(unique=True) port = models.IntegerField(default=22) username = models.CharField(max_length=64) auth_type = models.CharField(max_length=16, choices=[('key', '密钥'), ('pwd', '密码')]) credential = models.TextField() # 实际项目应加密存储 group = models.CharField(max_length=64, db_index=True) is_active = models.BooleanField(default=True) class Meta: indexes = [models.Index(fields=['group', 'is_active'])] class TaskTemplate(models.Model): name = models.CharField(max_length=128, unique=True) content = models.TextField() # shell 或 python 脚本 timeout = models.IntegerField(default=300) created_at = models.DateTimeField(auto_now_add=True) class TaskExecution(models.Model): template = models.ForeignKey(TaskTemplate, on_delete=models.CASCADE) status = models.CharField(max_length=16, default='pending') created_at = models.DateTimeField(auto_now_add=True) class TaskDetail(models.Model): execution = models.ForeignKey(TaskExecution, related_name='details', on_delete=models.CASCADE) host = models.ForeignKey(Host, on_delete=models.CASCADE) exit_code = models.IntegerField(null=True) stdout = models.TextField(blank=True) stderr = models.TextField(blank=True)

逻辑说明:Host 的 credential 字段在真实项目里必须用 Fernet 或 KMS 加密,直接明文存是血泪教训。TaskDetail 用 related_name='details',前端查一次执行的所有机器结果就是 execution.details.all(),不用手写 join。参数说明:timeout 默认 300 秒,批量任务里单机超时应该小于整体超时,否则一台卡死会拖垮整个批次。group 加索引是因为按业务组筛选主机是最高频查询。

2.2 用 Django 创建 app 并跑通最小可运行骨架

热词里“django创建app”是新手第一个卡点。运维平台建议按域拆 app,不要全塞在一个 app 里。常见做法是拆成 host、task、account 三个 app,host 管资产,task 管任务和执行,account 管用户和权限。

# 创建项目和 app django-admin startproject opsplatform cd opsplatform python manage.py startapp host python manage.py startapp task python manage.py startapp account # settings.py 里注册 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'host', 'task', 'account', ] # 生成迁移并建表 python manage.py makemigrations python manage.py migrate python manage.py createsuperuser

逻辑说明:startapp 之后必须把 app 名写进 INSTALLED_APPS,否则 makemigrations 不会扫描到模型。参数说明:createsuperuser 创建的账号用于登录 Django Admin,运维平台初期可以直接用 Admin 管理主机资产,省掉自研 CRUD 页面的时间。注意:数据库默认是 SQLite,生产环境要换成 PostgreSQL,因为批量任务写入并发高,SQLite 的写锁会成为瓶颈。

2.3 主机资产批量导入与连接测试

资产录入如果靠手工一条条填,平台还没上线运维就先疯了。常见做法是提供一个 CSV 导入接口,导入后异步做一次 SSH 连通性测试,把不可达的主机标记出来。

# host/services.py import paramiko from concurrent.futures import ThreadPoolExecutor from .models import Host def check_host(host: Host) -> bool: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: if host.auth_type == 'key': key = paramiko.RSAKey.from_private_key_file(host.credential) client.connect(host.ip, port=host.port, username=host.username, pkey=key, timeout=5) else: client.connect(host.ip, port=host.port, username=host.username, password=host.credential, timeout=5) return True except Exception: return False finally: client.close() def batch_check(host_ids): hosts = Host.objects.filter(id__in=host_ids) with ThreadPoolExecutor(max_workers=20) as pool: results = list(pool.map(check_host, hosts)) for host, ok in zip(hosts, results): host.is_active = ok Host.objects.bulk_update(hosts, ['is_active'])

逻辑说明:用线程池并发探测,20 个 worker 是经验值,再高容易触发目标机器的 SSH 连接数限制。参数说明:timeout=5 秒,内网可以设 3 秒,跨机房设 8 秒。bulk_update 一次性写回,避免逐条 save 产生大量 SQL。注意:AutoAddPolicy 会自动接受未知主机指纹,生产环境应改为 WarningPolicy 并维护 known_hosts,否则有中间人风险。

3. 任务执行引擎:从单机命令到批量并发

3.1 为什么不能用 Django 请求线程直接跑批量任务

新手最容易翻车的地方,是在 view 里直接 for 循环 SSH 执行,请求超时了任务还在跑,用户刷新页面又触发一次。正确做法是 view 只负责创建 TaskExecution 记录并投递到队列,由独立的 worker 消费。常见方案是 Celery + Redis,轻量场景也可以用 Django 的 management command 配合数据库轮询。

# task/tasks.py from celery import shared_task from .models import TaskExecution, TaskDetail from host.models import Host import paramiko @shared_task def run_task(execution_id): execution = TaskExecution.objects.get(id=execution_id) execution.status = 'running' execution.save(update_fields=['status']) hosts = Host.objects.filter(is_active=True) for host in hosts: detail = TaskDetail.objects.create(execution=execution, host=host) client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: client.connect(host.ip, port=host.port, username=host.username, password=host.credential, timeout=5) stdin, stdout, stderr = client.exec_command( execution.template.content, timeout=execution.template.timeout) detail.exit_code = stdout.channel.recv_exit_status() detail.stdout = stdout.read().decode('utf-8', errors='ignore') detail.stderr = stderr.read().decode('utf-8', errors='ignore') except Exception as e: detail.exit_code = -1 detail.stderr = str(e) finally: client.close() detail.save() execution.status = 'finished' execution.save(update_fields=['status'])

逻辑说明:exec_command 返回的三个通道必须都读,只读 stdout 不读 stderr 在输出量大时会阻塞。参数说明:recv_exit_status 会阻塞直到命令结束,配合 timeout 参数控制单机最长执行时间。注意:这个版本是串行执行,生产环境要改成 ThreadPoolExecutor 或 Celery 的 group,但并发数要控制,否则一次对 500 台机器发命令,网络设备先扛不住。

3.2 任务模板的参数化与安全边界

硬编码命令的模板没有复用价值。常见做法是在模板里用 {{var}} 占位,执行时传入参数字典做替换。但这里有个安全红线:参数必须做白名单校验,否则就是命令注入。

# task/services.py import re PARAM_PATTERN = re.compile(r'\{\{(\w+)\}\}') def render_template(content: str, params: dict) -> str: def replace(match): key = match.group(1) if key not in params: raise ValueError(f'缺少参数: {key}') value = str(params[key]) # 只允许字母数字、点、斜杠、横线、下划线 if not re.match(r'^[\w./-]+$', value): raise ValueError(f'参数 {key} 含非法字符') return value return PARAM_PATTERN.sub(replace, content)

逻辑说明:用正则替换而不是 str.format,因为 format 会执行属性访问,存在注入风险。参数说明:白名单正则 ^[\w./-]+$ 覆盖了路径、版本号、服务名等常见参数,如果业务需要空格或引号,要单独评估。注意:这个校验放在服务端,前端校验只是体验优化,不能作为安全依据。

3.3 执行结果的存储与查询优化

批量任务跑完,TaskDetail 表会迅速膨胀。一台机器一天跑 10 个任务,100 台机器一个月就是 3 万条明细。常见做法是 stdout/stderr 超过一定长度就截断存储,完整日志落文件或对象存储,数据库只存路径和摘要。

MAX_LOG = 8192 def save_detail(detail, stdout, stderr): detail.stdout = stdout[:MAX_LOG] detail.stderr = stderr[:MAX_LOG] if len(stdout) > MAX_LOG: detail.stdout += '\n...[已截断]' detail.save()

逻辑说明:截断阈值 8KB 是经验值,足够定位大多数报错。参数说明:如果业务需要完整日志,把完整内容写到 /var/log/ops/{execution_id}/{host_ip}.log,数据库存路径。注意:TaskDetail 表要按 created_at 建索引,并且定期归档,否则半年后查询会明显变慢。

4. 避坑与排查:那些让平台上线即翻车的细节

4.1 现象:批量任务卡在 running 状态不结束

原因:worker 进程被 OOM kill 或者 Celery 任务没有设置超时,某台机器 SSH 连接挂起导致整个任务阻塞。解决:给 Celery 任务加 soft_time_limit 和 time_limit,SSH 连接设置 timeout 和 banner_timeout,并且用 try/finally 保证异常时也更新 execution 状态。

4.2 现象:同一任务被重复执行两次

原因:前端提交按钮没有防抖,或者 Celery 的 acks_late 配置导致任务重投。解决:在 TaskExecution 创建时用数据库唯一约束或 Redis 锁做幂等,前端按钮点击后立即 disable。acks_late 要配合任务幂等设计使用,不能盲目开。

4.3 现象:中文输出乱码

原因:目标机器 locale 不是 UTF-8,stdout.read() 按默认编码解码失败。解决:decode 时显式指定 errors='ignore',或者在 exec_command 前加 export LANG=en_US.UTF-8。更稳妥的做法是统一用 bytes 存储,展示层再解码。

4.4 现象:主机密钥认证失败但密码认证正常

原因:paramiko 读取的私钥格式不对,比如 OpenSSH 新格式需要额外转换,或者私钥有 passphrase 没传。解决:用 paramiko.RSAKey.from_private_key_file 时捕获 PasswordRequiredException,提示用户私钥需要去密码;新格式私钥先用 ssh-keygen -p -m PEM 转换。

4.5 现象:Django Admin 加载主机列表越来越慢

原因:Host 表数据量大,Admin 默认的 count 查询和每行外键查询拖慢页面。解决:在 ModelAdmin 里设置 list_select_related、list_per_page=50,关闭 show_full_result_count,必要时用 raw_id_fields 替代下拉框。

5. 进阶技巧:把执行记录变成可检索的运维知识库

平台跑起来之后,最有价值的不是执行本身,而是积累下来的失败记录。我习惯在 TaskDetail 上加一个 failure_signature 字段,把 stderr 做归一化(去掉 IP、时间戳、PID)后取 hash,相同签名归为一类。这样跑一个月后,你能直接看到“Top 10 失败原因”,而不是一条条翻日志。

import hashlib import re def normalize(stderr: str) -> str: s = re.sub(r'\d+\.\d+\.\d+\.\d+', 'IP', stderr) s = re.sub(r'\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}', 'TIME', s) s = re.sub(r'\b\d+\b', 'N', s) return s.strip() def signature(stderr: str) -> str: return hashlib.md5(normalize(stderr).encode()).hexdigest()[:16]

逻辑说明:归一化把变量替换成占位符,让同类错误落到同一个签名。参数说明:hash 取前 16 位足够区分,存 char(16) 即可。注意:这个函数要在保存 TaskDetail 时调用,历史数据可以写个 management command 回填。

另一个技巧是给任务模板加“最近成功率”统计,用 Django 的 annotate 一次查出,展示在模板列表页。运维看到某个模板成功率从 99% 掉到 80%,就知道该去查原因了。

from django.db.models import Count, Q templates = TaskTemplate.objects.annotate( total=Count('taskexecution__details'), failed=Count('taskexecution__details', filter=Q(taskexecution__details__exit_code__gt=0)) ) for t in templates: rate = (t.total - t.failed) / t.total if t.total else 0 print(t.name, f'{rate:.1%}')

逻辑说明:用条件聚合一次查询算出总数和失败数,避免 N+1。参数说明:exit_code__gt=0 表示非零退出码算失败,-1 表示连接异常也算失败。注意:数据量大时这个查询会慢,可以做成定时任务写入统计表,页面直接读结果。

我自己的习惯是,平台上线第一周不接生产批量操作,只做只读的资产巡检和日志采集,让团队先信任执行结果的准确性。等失败签名库积累到几十条,再逐步开放写操作。这个节奏比一上来就全量接管稳得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询