第 11 章:NAS 核心模块分步实现
用 AI 逐一实现存储、共享、用户、Web 四大核心模块
🎯 本章学习目标
- 完整实现存储管理模块:磁盘发现、挂载、RAID、SMART 监控
- 实现网络服务管理:Samba、NFS 的配置与动态管理
- 构建用户与权限系统:认证、授权、配额管理
- 开发Web 管理后台:FastAPI + Vue 3 全栈实现
- 掌握AI 协作编程的完整实战流程
11.1 模块 1:存储管理系统
核心类设计
"""nas/storage/disk_manager.py — 磁盘管理核心模块
使用 AI 生成的提示词模板:
角色:Linux 系统编程专家
任务:实现磁盘发现、信息获取、挂载管理
约束:Python 3.11+, 系统命令通过 CommandExecutor 封装
"""
import subprocess
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
@dataclass
class DiskInfo:
"""磁盘信息数据模型"""
device: str # /dev/sda
model: str = "" # WDC WD40EFRX-68N
serial: str = "" # 序列号
size_bytes: int = 0 # 总字节数
transport: str = "" # sata / usb / nvme
rotational: bool = True # True=HDD, False=SSD
partitions: list[dict] = field(default_factory=list)
smart_status: str = "unknown" # ok / warning / error / unknown
smart_attrs: dict = field(default_factory=dict)
class DiskManager:
"""磁盘管理器 — 发现、信息获取、健康监控"""
@staticmethod
def list_disks(include_partitions: bool = False) -> list[DiskInfo]:
"""通过 lsblk 发现所有块设备"""
cmd = ["lsblk", "-J", "-o",
"NAME,SIZE,MODEL,SERIAL,TRAN,ROTA,MOUNTPOINT,FSTYPE,UUID"]
if not include_partitions:
cmd.insert(2, "-d") # 只显示顶层设备
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
raise RuntimeError(f"lsblk 执行失败: {result.stderr}")
data = json.loads(result.stdout)
disks = []
for dev in data.get("blockdevices", []):
disk = DiskInfo(
device=f"/dev/{dev['name']}",
model=dev.get("model", ""),
serial=dev.get("serial", ""),
size_bytes=dev.get("size", 0),
transport=dev.get("tran", ""),
rotational=dev.get("rota", True),
partitions=dev.get("children", []),
)
# 获取 SMART 状态
disk.smart_status = DiskManager._get_smart_status(disk.device)
disks.append(disk)
return disks
@staticmethod
def get_disk_detail(device: str) -> Optional[DiskInfo]:
"""获取单个磁盘的详细信息"""
disks = DiskManager.list_disks()
for d in disks:
if d.device == device:
d.smart_attrs = DiskManager._get_smart_attributes(device)
return d
return None
@staticmethod
def _get_smart_status(device: str) -> str:
"""获取 SMART 健康状态"""
try:
result = subprocess.run(
["smartctl", "-H", device],
capture_output=True, text=True, timeout=30
)
if "PASSED" in result.stdout:
return "ok"
elif "FAILED" in result.stdout:
return "error"
return "unknown"
except (subprocess.TimeoutExpired, FileNotFoundError):
return "unknown"
@staticmethod
def _get_smart_attributes(device: str) -> dict:
"""获取详细 SMART 属性"""
try:
result = subprocess.run(
["smartctl", "-A", device],
capture_output=True, text=True, timeout=30
)
attrs = {}
for line in result.stdout.split("\n"):
parts = line.strip().split()
if len(parts) >= 10 and parts[0].isdigit():
attrs[parts[1]] = {
"id": int(parts[0]),
"value": int(parts[3]),
"worst": int(parts[4]),
"threshold": int(parts[5]),
"raw_value": parts[9],
}
return attrs
except Exception:
return {}
class MountManager:
"""挂载管理器 — 挂载、卸载、fstab 管理"""
MOUNT_BASE = Path("/mnt/nas")
@classmethod
def mount(cls, device: str, mount_point: str, fs_type: str = "auto",
options: list[str] = None) -> bool:
"""挂载设备到指定目录"""
target = cls.MOUNT_BASE / mount_point.lstrip("/")
target.mkdir(parents=True, exist_ok=True)
cmd = ["mount"]
if fs_type != "auto":
cmd.extend(["-t", fs_type])
if options:
cmd.extend(["-o", ",".join(options)])
cmd.extend([device, str(target)])
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise OSError(f"挂载失败: {result.stderr.strip()}")
return True
@classmethod
def unmount(cls, mount_point: str, force: bool = False) -> bool:
"""卸载设备"""
target = cls.MOUNT_BASE / mount_point.lstrip("/")
cmd = ["umount"]
if force:
cmd.append("-f")
cmd.append(str(target))
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise OSError(f"卸载失败: {result.stderr.strip()}")
return True
@classmethod
def get_mounts(cls) -> list[dict]:
"""获取当前所有挂载点"""
with open("/proc/mounts") as f:
mounts = []
for line in f:
parts = line.strip().split()
if len(parts) >= 4:
mounts.append({
"device": parts[0],
"mount_point": parts[1],
"fs_type": parts[2],
"options": parts[3],
})
return mounts
11.2 模块 2:网络服务管理
Samba 共享管理器
"""nas/services/samba_manager.py — SMB 共享管理"""
import subprocess
import tempfile
import shutil
from pathlib import Path
from dataclasses import dataclass, field
@dataclass
class SMBShareConfig:
"""SMB 共享配置"""
name: str
path: str
comment: str = ""
browseable: bool = True
read_only: bool = False
guest_ok: bool = False
valid_users: list[str] = field(default_factory=list)
write_list: list[str] = field(default_factory=list)
create_mask: str = "0664"
directory_mask: str = "0775"
class SambaManager:
"""Samba 服务管理器"""
SMB_CONF = Path("/etc/samba/smb.conf")
BACKUP_DIR = Path("/etc/samba/backups")
@classmethod
def add_share(cls, config: SMBShareConfig) -> bool:
"""添加 SMB 共享"""
# 验证共享名
if not config.name or len(config.name) > 15:
raise ValueError("共享名必须是 1-15 个字符")
if not Path(config.path).exists():
Path(config.path).mkdir(parents=True, exist_ok=True)
# 生成配置段
section = cls._generate_config_section(config)
# 备份原配置
cls._backup_config()
# 追加配置
with open(cls.SMB_CONF, 'a') as f:
f.write(f"\n{section}\n")
# 重载服务
return cls.reload()
@classmethod
def remove_share(cls, name: str) -> bool:
"""删除 SMB 共享"""
cls._backup_config()
content = cls.SMB_CONF.read_text()
# 移除 [share_name] 段(包括其所有配置行)
pattern = rf'\n?\s*\[{re.escape(name)}\].*?(?=\n\s*\[|\Z)'
content = re.sub(pattern, '', content, flags=re.DOTALL)
cls.SMB_CONF.write_text(content)
return cls.reload()
@classmethod
def list_shares(cls) -> list[dict]:
"""列出所有自定义共享"""
content = cls.SMB_CONF.read_text()
shares = []
in_share = False
current = {}
for line in content.split("\n"):
line = line.strip()
if line.startswith("[") and line.endswith("]"):
if in_share and current:
shares.append(current)
name = line[1:-1]
# 跳过全局段和默认共享
if name not in ("global", "homes", "printers", "print$"):
in_share = True
current = {"name": name}
else:
in_share = False
current = {}
elif in_share and "=" in line:
key, val = line.split("=", 1)
current[key.strip()] = val.strip()
if in_share and current:
shares.append(current)
return shares
@classmethod
def reload(cls) -> bool:
"""重载 Samba 配置"""
result = subprocess.run(
["systemctl", "reload", "smbd"],
capture_output=True, text=True, timeout=10
)
return result.returncode == 0
@staticmethod
def _generate_config_section(config: SMBShareConfig) -> str:
lines = [
f"[{config.name}]",
f" path = {config.path}",
]
if config.comment:
lines.append(f" comment = {config.comment}")
lines.append(f" browseable = {'yes' if config.browseable else 'no'}")
lines.append(f" read only = {'yes' if config.read_only else 'no'}")
lines.append(f" guest ok = {'yes' if config.guest_ok else 'no'}")
if config.valid_users:
lines.append(f" valid users = {' '.join(config.valid_users)}")
if config.write_list:
lines.append(f" write list = {' '.join(config.write_list)}")
lines.append(f" create mask = {config.create_mask}")
lines.append(f" directory mask = {config.directory_mask}")
return "\n".join(lines)
@classmethod
def _backup_config(cls):
cls.BACKUP_DIR.mkdir(parents=True, exist_ok=True)
import time
stamp = time.strftime("%Y%m%d_%H%M%S")
shutil.copy2(cls.SMB_CONF, cls.BACKUP_DIR / f"smb.conf.{stamp}")
11.3 模块 3:用户与权限系统
"""nas/auth/user_manager.py — 用户认证与权限管理"""
import subprocess
import pwd
import grp
import os
from dataclasses import dataclass
from typing import Optional
@dataclass
class UserInfo:
username: str
uid: int
gid: int
groups: list[str]
home: str
shell: str
quota_gb: float = 0
class UserManager:
"""NAS 用户管理器 — PAM 认证 + 本地用户管理"""
@staticmethod
def list_users(min_uid: int = 1000) -> list[UserInfo]:
"""列出所有普通用户"""
users = []
for entry in pwd.getpwall():
if entry.pw_uid >= min_uid and entry.pw_uid < 65534:
groups = [
g.gr_name for g in grp.getgrall()
if entry.pw_name in g.gr_mem
]
# 获取用户主组
main_group = grp.getgrgid(entry.pw_gid)
if main_group.gr_name not in groups:
groups.insert(0, main_group.gr_name)
users.append(UserInfo(
username=entry.pw_name,
uid=entry.pw_uid,
gid=entry.pw_gid,
groups=groups,
home=entry.pw_dir,
shell=entry.pw_shell,
))
return users
@classmethod
def create_user(cls, username: str, password: str,
groups: list[str] = None, shell: str = "/bin/bash") -> bool:
"""创建 NAS 用户"""
# 验证用户名
if not username or not username.isalnum():
raise ValueError("用户名只能包含字母和数字")
# 检查是否已存在
try:
pwd.getpwnam(username)
raise ValueError(f"用户 {username} 已存在")
except KeyError:
pass
# 创建用户
home = f"/home/{username}"
cmd = ["useradd", "-m", "-d", home, "-s", shell, username]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"用户创建失败: {result.stderr}")
# 设置密码
set_pw = subprocess.run(
["chpasswd"],
input=f"{username}:{password}",
capture_output=True, text=True
)
if set_pw.returncode != 0:
# 回滚
subprocess.run(["userdel", "-r", username])
raise RuntimeError(f"密码设置失败: {set_pw.stderr}")
# 添加到附加组
if groups:
subprocess.run(["usermod", "-aG", ",".join(groups), username])
return True
@classmethod
def delete_user(cls, username: str, remove_home: bool = True) -> bool:
"""删除用户"""
# 防止删除系统用户
entry = pwd.getpwnam(username)
if entry.pw_uid < 1000:
raise ValueError(f"不能删除系统用户: {username}")
cmd = ["userdel"]
if remove_home:
cmd.append("-r")
cmd.append(username)
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
@classmethod
def set_quota(cls, username: str, soft_gb: float, hard_gb: float,
filesystem: str = "/") -> bool:
"""设置用户磁盘配额"""
soft_kb = int(soft_gb * 1024 * 1024)
hard_kb = int(hard_gb * 1024 * 1024)
result = subprocess.run(
["setquota", "-u", username,
str(soft_kb), str(hard_kb), "0", "0", filesystem],
capture_output=True, text=True
)
return result.returncode == 0
@staticmethod
def authenticate(username: str, password: str) -> bool:
"""通过 PAM 验证用户密码"""
import pwd
import crypt
import spwd
try:
shadow = spwd.getspnam(username)
hashed = shadow.sp_pwd if shadow.sp_pwd != "!" else None
if not hashed:
return False
return crypt.crypt(password, hashed) == hashed
except (KeyError, PermissionError):
return False
📋 本章核心提示词模板
模块实现的标准提示词模板
📋
## 角色
你是一位 [NAS 存储系统 / Linux 网络服务 / 安全认证系统] 专家。
## 任务
为 NAS 操作系统实现 [模块名] 模块。
### 功能需求
[列出具体功能,每个功能一行,可验证的]
### 接口契约
模块必须提供以下公开接口:
- [方法签名] — [功能描述]
- [方法签名] — [功能描述]
## 约束
- Python 3.11+,类型注解完整
- 所有系统命令通过 subprocess 调用,带超时
- 所有公开方法有 Google-style docstring
- 错误处理:系统调用失败 → 抛出自定义异常(不是 None 或 False)
- 线程安全(如果涉及共享状态)
## 输出
1. 完整的 Python 模块文件
2. 单元测试文件(使用 pytest)
3. 使用示例代码
🛠️ 实战演练:Web API 端点实现
用 AI 生成 FastAPI 磁盘管理 API
提示词
请为 NAS 磁盘管理模块生成 FastAPI 路由。
DiskManager 和 MountManager 类已实现(文件名 disk_manager.py)。
需要的端点:
GET /api/disks — 列出所有磁盘(含 SMART 状态)
GET /api/disks/{device} — 获取单个磁盘详情
POST /api/disks/mount — 挂载磁盘
POST /api/disks/unmount — 卸载磁盘
GET /api/disks/mounts — 列出所有挂载点
要求:
- 使用 FastAPI 依赖注入获取 DiskManager 实例
- 所有端点需要 JWT 认证(使用已有的 get_current_user 依赖)
- 请求/响应使用 Pydantic 模型
- 错误响应统一格式:{"detail": "错误描述"}
- 返回中文错误消息
AI 输出
"""nas/api/disk_routes.py — 磁盘管理 API 路由"""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from typing import Optional
from nas.storage.disk_manager import DiskManager, MountManager
from nas.auth.dependencies import get_current_user
router = APIRouter(prefix="/api/disks", tags=["磁盘管理"])
class MountRequest(BaseModel):
device: str = Field(..., description="设备路径,如 /dev/sdb1")
mount_point: str = Field(..., description="挂载点,如 data/backup")
fs_type: str = Field(default="auto", description="文件系统类型")
options: Optional[list[str]] = Field(default=None)
class UnmountRequest(BaseModel):
mount_point: str = Field(..., description="挂载点路径")
force: bool = Field(default=False, description="强制卸载")
@router.get("/", summary="列出所有磁盘")
async def list_disks(user=Depends(get_current_user)):
"""获取系统中所有块设备及其 SMART 健康状态"""
try:
disks = DiskManager.list_disks()
return {
"total": len(disks),
"disks": [
{
"device": d.device,
"model": d.model,
"size_bytes": d.size_bytes,
"size_human": DiskManager._format_size(d.size_bytes),
"transport": d.transport,
"type": "HDD" if d.rotational else "SSD",
"smart_status": d.smart_status,
}
for d in disks
]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取磁盘列表失败: {str(e)}")
@router.get("/{device}", summary="获取磁盘详情")
async def get_disk_detail(device: str, user=Depends(get_current_user)):
"""获取指定磁盘的详细信息(含 SMART 属性)"""
try:
info = DiskManager.get_disk_detail(f"/dev/{device}")
if not info:
raise HTTPException(status_code=404, detail=f"磁盘 {device} 不存在")
return {
"device": info.device,
"model": info.model,
"serial": info.serial,
"size_bytes": info.size_bytes,
"smart_status": info.smart_status,
"smart_attributes": info.smart_attrs,
"partitions": info.partitions,
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/mount", summary="挂载磁盘")
async def mount_disk(req: MountRequest, user=Depends(get_current_user)):
"""将磁盘分区挂载到指定目录"""
try:
MountManager.mount(req.device, req.mount_point, req.fs_type, req.options)
return {"status": "ok", "device": req.device, "mount_point": req.mount_point}
except OSError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/unmount", summary="卸载磁盘")
async def unmount_disk(req: UnmountRequest, user=Depends(get_current_user)):
"""卸载已挂载的磁盘"""
try:
MountManager.unmount(req.mount_point, req.force)
return {"status": "ok", "mount_point": req.mount_point}
except OSError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/mounts/list", summary="列出挂载点")
async def list_mounts(user=Depends(get_current_user)):
"""获取当前系统的所有挂载点"""
return {"mounts": MountManager.get_mounts()}
⚠️ 常见坑点
🕳️ 坑 1:系统命令超时不处理
mount、umount、smartctl 等命令在异常磁盘上可能挂起数分钟。所有 subprocess.run 必须带 timeout 参数。
🕳️ 坑 2:直接拼接 shell 命令
永远不要用 shell=True 或 os.system。用 subprocess.run([...]) 的列表形式,避免命令注入。
🕳️ 坑 3:修改系统配置不备份
修改 /etc/samba/smb.conf、/etc/fstab 等文件前必须备份。使用 timestamp + copy2 的组合。
📝 本章小结
- 存储管理:通过 lsblk/smartctl/mount 等系统命令封装,AI 帮助生成类型安全的数据模型和错误处理
- 网络服务:Samba 配置管理遵循"备份-修改-重载"的安全流程
- 用户管理:PAM + crypt 认证,配额管理通过 setquota 系统命令实现
- Web API:FastAPI + Pydantic 模型,所有端点需要 JWT 认证
- 每个模块的 AI 生成都遵循相同的角色-任务-约束-输出模板
🤔 思考练习
- 用本章的代码框架,让 AI 补充实现 NFS 导出管理器(NFSManager)。
- 为 UserManager 编写完整的 pytest 测试套件,包括 Mock 系统调用。
- 用 AI 生成一个磁盘性能基准测试工具(使用 fio 或 dd 命令),并集成到 DiskManager 中。