第318篇:Git 配置管理
关键词
Git、版本控制、分支策略、配置仓库、Git Hooks、Merge Request、配置审计、团队协作
一、Git 在网络自动化中的角色
Git 在网络自动化中的三层作用:
1. 版本控制
┌─ 每次配置变更都有记录
├─ 谁在何时改了什么都可追溯
├─ 支持回滚到任意历史版本
└─ diff 显示变更细节
2. 团队协作
┌─ 多人并行开发不同分支
├─ MR/PR 代码审查
├─ 冲突解决
└─ 权限控制(谁可以改什么)
3. CI/CD 触发
┌─ Git Push 触发自动化流水线
├─ 分支对应不同环境
├─ Tag 对应发布版本
└─ Webhook 集成
1.1 网络配置仓库分支策略
推荐分支策略(Git Flow 简化版):
master(生产) └─ 与生产设备 running-config 一致 └─ 只有经过审批的 MR 才能合入 staging(预生产) └─ 与预生产环境配置一致 └─ 合入后自动部署到预生产 develop(开发) └─ 日常开发分支 └─ 所有 feature 分支从这里拉出 feature/xxx(特性分支) └─ 每个变更一个分支 └─ 命名:feature/add-vlan-100 └─ 完成后 MR → develop hotfix/xxx(紧急修复) └─ 生产问题快速修复 └─ 直接 MR → master + develop release/x.x(发布分支) └─ 从 develop 拉出,准备发布 └─ 测试通过后合入 master + tag
二、Git 操作实战
2.1 仓库初始化
# 创建网络配置仓库
mkdir network-config
cd network-config
git init
# 创建目录结构
mkdir -p config templates scripts tests baselines
# 初始提交
git add .
git commit -m "chore: 初始化网络配置仓库
- 创建仓库目录结构
- 添加 .gitignore
- 添加 README"
# 关联远程仓库
git remote add origin git@git.corp.com:network/config.git
git push -u origin master
2.2 日常变更流程
# 1. 从 develop 拉取新特性分支
git checkout develop
git pull
git checkout -b feature/add-vlan-200
# 2. 修改配置
# 编辑 config/vlans.yml
# 编辑 templates/access_switch.j2
# 3. 验证
python scripts/validate.py
python scripts/render.py
# 4. 暂存并提交
git add config/vlans.yml templates/access_switch.j2
git commit -m "feat: 新增 VLAN 200 (DEV)
- config/vlans.yml: VLAN 200 定义
- templates/access_switch.j2: 接入交换机模板
- 验证通过"
# 5. 推送并创建 MR
git push -u origin feature/add-vlan-200
# 在 GitLab/GitHub 创建 Merge Request
# 6. MR 审查后合并
# 在 Web UI 上完成 Code Review
# 点击 Merge → 自动触发 CI/CD
# 7. 删除已合并的分支
git checkout develop
git pull
git branch -d feature/add-vlan-200
2.3 Git Hooks 自动化
#!/bin/bash
# .git/hooks/pre-commit — 提交前自动验证
echo "=== Pre-commit 验证 ==="
# 检查 YAML 格式
echo "1. YAML 语法检查..."
for file in $(git diff --cached --name-only --diff-filter=AM | grep -E '\.(yml|yaml)$'); do
python -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null
if [ $? -ne 0 ]; then
echo "❌ $file: YAML 格式错误"
exit 1
fi
echo " ✓ $file"
done
# 检查 JSON 格式
echo "2. JSON 语法检查..."
for file in $(git diff --cached --name-only --diff-filter=AM | grep '\.json$'); do
python -c "import json; json.load(open('$file'))" 2>/dev/null
if [ $? -ne 0 ]; then
echo "❌ $file: JSON 格式错误"
exit 1
fi
echo " ✓ $file"
done
# 检查 Python 语法
echo "3. Python 语法检查..."
for file in $(git diff --cached --name-only --diff-filter=AM | grep '\.py$'); do
python -m py_compile "$file" 2>/dev/null
if [ $? -ne 0 ]; then
echo "❌ $file: Python 语法错误"
exit 1
fi
echo " ✓ $file"
done
# 检查是否包含明文密码
echo "4. 敏感信息检查..."
for file in $(git diff --cached --name-only --diff-filter=AM); do
if grep -q -E '(password|secret|passwd)\s*[:=]\s*[^$]' "$file" 2>/dev/null; then
# 排除加密的 vault 文件
if ! echo "$file" | grep -q "\.vault"; then
echo "⚠ $file: 可能包含明文密码"
fi
fi
done
echo "=== Pre-commit 验证通过 ==="
2.4 .gitignore 配置
# .gitignore — 网络配置仓库忽略规则
# Python 缓存
__pycache__/
*.pyc
*.pyo
*.egg-info/
# 虚拟环境
venv/
.venv/
env/
# 渲染输出
rendered/
output/
# 备份文件
backup/
*.bak
*.cfg.bak
# 敏感信息
**/secrets.yml
**/passwords.yml
*.vault
!example.secrets.yml
# 日志
logs/
*.log
# 编辑器
.vscode/
.idea/
*.swp
*.swo
# 操作系统
.DS_Store
Thumbs.db
# 临时文件
*.tmp
*.temp
三、Git 操作 Python 封装
#!/usr/bin/env python3
# scripts/git_operations.py — Git 操作封装
import subprocess
import os
from typing import List, Optional
from datetime import datetime
class GitManager:
"""Git 仓库管理器"""
def __init__(self, repo_path: str = "."):
self.repo_path = os.path.abspath(repo_path)
def _run_git(self, args: List[str]) -> str:
"""执行 Git 命令"""
cmd = ["git"] + args
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=self.repo_path,
)
if result.returncode != 0:
raise RuntimeError(
f"Git 命令失败: {' '.join(cmd)}\n{result.stderr}"
)
return result.stdout.strip()
@property
def current_branch(self) -> str:
"""当前分支名"""
return self._run_git(["rev-parse", "--abbrev-ref", "HEAD"])
@property
def last_commit(self) -> str:
"""最后一次提交信息"""
return self._run_git(["log", "-1", "--pretty=format:%h %s"])
def status(self) -> str:
"""查看仓库状态"""
return self._run_git(["status"])
def diff(self, since: str = "HEAD~1") -> str:
"""查看最近变更"""
return self._run_git(["diff", since, "--stat"])
def changed_files(self, since: str = "HEAD~1") -> List[str]:
"""获取变更文件列表"""
output = self._run_git(["diff", since, "--name-only"])
return [f for f in output.split("\n") if f]
def commit_and_push(self, message: str, branch: str = None) -> bool:
"""提交并推送"""
try:
# 暂存所有变更
self._run_git(["add", "-A"])
# 检查是否有变更
status = self._run_git(["status", "--porcelain"])
if not status:
print("无变更需要提交")
return False
# 提交
self._run_git(["commit", "-m", message])
commit_hash = self._run_git(["rev-parse", "HEAD"])
print(f"✓ 提交成功: {commit_hash[:8]} - {message}")
# 推送
target_branch = branch or self.current_branch
self._run_git(["push", "-u", "origin", target_branch])
print(f"✓ 已推送到 origin/{target_branch}")
return True
except RuntimeError as e:
print(f"✗ Git 操作失败: {e}")
return False
def create_branch(self, branch_name: str, base: str = "develop"):
"""创建分支"""
self._run_git(["checkout", base])
self._run_git(["pull"])
self._run_git(["checkout", "-b", branch_name])
print(f"✓ 已创建分支: {branch_name} (基于 {base})")
def get_commit_history(self, path: str = "", limit: int = 10) -> List[dict]:
"""获取提交历史"""
args = [
"log", f"--max-count={limit}",
"--pretty=format:%H|%an|%ad|%s",
"--date=iso",
]
if path:
args.append("--")
args.append(path)
output = self._run_git(args)
commits = []
for line in output.split("\n"):
if not line:
continue
parts = line.split("|", 3)
if len(parts) == 4:
commits.append({
"hash": parts[0][:8],
"author": parts[1],
"date": parts[2],
"message": parts[3],
})
return commits
def tag_release(self, version: str, message: str = ""):
"""创建发布标签"""
msg = message or f"Release {version}"
self._run_git(["tag", "-a", version, "-m", msg])
self._run_git(["push", "origin", version])
print(f"✓ 已创建标签: {version}")
def get_config_diff(self, device_name: str) -> str:
"""获取特定设备的配置变更"""
pattern = f"**/{device_name}*"
return self._run_git([
"log", "-p", "--all",
"--diff-filter=M",
"--", f"*{device_name}*",
"-n", "5",
])
# 使用示例
if __name__ == "__main__":
git = GitManager(".")
print(f"当前分支: {git.current_branch}")
print(f"最近提交: {git.last_commit}")
print()
# 查看最近变更
print("=== 最近变更文件 ===")
for f in git.changed_files():
print(f" {f}")
# 提交历史
print("\n=== 提交历史 ===")
for c in git.get_commit_history(limit=5):
print(f" {c['hash']} {c['author']} {c['date'][:10]}: {c['message']}")
四、Merge Request 规范
4.1 MR 模板
<!-- .gitlab/merge_request_templates/network_change.md -->
## 变更描述
**变更类型**: (新增/修改/删除/回滚)
**关联需求**: #123
## 变更范围
- [ ] 接入交换机
- [ ] 核心交换机
- [ ] 路由器
- [ ] 防火墙
## 变更内容
### 配置文件修改
- `config/vlans.yml`: 新增 VLAN 200
- `templates/access_switch.j2`: 模板更新
### 变更摘要
- vlan 200
- name DEV
## 验证结果
- [x] YAML 语法检查通过
- [x] 模板渲染验证通过
- [x] IP 冲突检测通过
- [x] 合规策略检查通过
- [ ] 测试环境验证通过
## 风险评估
- 影响范围: ACC-SW01 ~ ACC-SW05
- 回滚方案: git revert + 重新部署
- 变更窗口: 2025-01-15 02:00-04:00
## 审批人
- [ ] @network-lead
- [ ] @change-manager
4.2 提交信息规范
Commit Message 规范:
<type>(<scope>): <subject>
types:
┌─ feat: 新功能(新增 VLAN/接口/BGP)
├─ fix: 修复(配置错误/Bug)
├─ config: 配置变更
├─ refactor:重构
├─ test: 测试
├─ docs: 文档
└─ chore: 杂项(CI/目录结构)
示例:
feat(vlan): 新增 VLAN 200 用于开发环境
fix(bgp): 修复邻居 10.0.0.1 的 AS 号配置错误
config(ntp): 更新 NTP 服务器 IP
chore(ci): 添加模板验证 Job
refactor(templates): 抽取宏定义到 macros.j2
五、配置审计与合规
5.1 Git 审计
# 谁在何时改了什么?
git log --oneline --author="zhangsan"
git log --after="2025-01-01" --before="2025-01-15"
# 查看特定文件的修改历史
git log -p -- config/vlans.yml
# 对比两个版本的差异
git diff v1.0 v1.1 -- config/devices.yml
# 查看某行代码是谁改的(blame)
git blame templates/access_switch.j2
# 生成变更报告
git log --pretty=format:"%h %an %ad %s" --date=short \
--since="2025-01-01" --until="2025-01-31" \
--no-merges > change_report.txt
5.2 合规检查脚本
#!/usr/bin/env python3
# scripts/audit_check.py — Git 审计检查
from git_operations import GitManager
import re
class GitAudit:
"""Git 审计检查"""
def __init__(self, repo_path: str):
self.git = GitManager(repo_path)
def check_commit_messages(self, since: str = "HEAD~20") -> list:
"""检查提交信息规范"""
violations = []
pattern = r'^(feat|fix|config|refactor|test|docs|chore)\([^)]+\): .+'
for c in self.git.get_commit_history(limit=100):
if not re.match(pattern, c["message"]):
violations.append(c)
return violations
def check_unauthorized_files(self) -> list:
"""检查是否包含敏感文件"""
sensitive_patterns = [
r'.*\.pem$',
r'.*\.key$',
r'.*password.*',
r'.*secret.*',
]
violations = []
changed = self.git.changed_files(since="HEAD~10")
for f in changed:
for pattern in sensitive_patterns:
if re.match(pattern, f):
violations.append(f)
return violations
def generate_audit_report(self, since: str = "last month") -> str:
"""生成审计报告"""
report = []
report.append("=" * 50)
report.append("Git 审计报告")
report.append(f"生成时间: {__import__('datetime').datetime.now()}")
report.append(f"审计范围: {since}")
report.append("=" * 50)
# 提交统计
commits = self.git.get_commit_history(limit=500)
authors = {}
for c in commits:
authors[c["author"]] = authors.get(c["author"], 0) + 1
report.append(f"\n提交总数: {len(commits)}")
report.append("\n提交统计:")
for author, count in sorted(authors.items(), key=lambda x: -x[1]):
report.append(f" {author}: {count} 次提交")
# 规范检查
bad_msgs = self.check_commit_messages()
if bad_msgs:
report.append(f"\n⚠ 不合规的提交信息 ({len(bad_msgs)} 条):")
for c in bad_msgs[:10]:
report.append(f" {c['hash']}: {c['message']}")
# 敏感文件
sensitive = self.check_unauthorized_files()
if sensitive:
report.append(f"\n❌ 可疑的敏感文件 ({len(sensitive)} 个):")
for f in sensitive:
report.append(f" {f}")
return "\n".join(report)
if __name__ == "__main__":
audit = GitAudit(".")
print(audit.generate_audit_report())
六、最佳实践
Git 配置管理最佳实践:
1. 仓库组织
┌─ 一个仓库管理全网配置
├─ 按设备角色/站点分目录
├─ 配置数据与模板分离
└─ 版本号与发布对应
2. 分支策略
┌─ master = 生产配置
├─ 每次变更走 feature 分支
├─ MR 必须有人审查
└─ 紧急修复走 hotfix 分支
3. 提交规范
┌─ 每次提交原子性(一件事)
├─ 提交信息描述"为什么"而非"改了什么"
├─ 关联 Issue/MR
└─ 不做格式化/重构提交
4. 安全
┌─ 永远不提交密码/密钥
├─ 使用 git-secrets 扫描
├─ .gitignore 排除敏感文件
└─ 历史中的密码用 git-filter-repo 清理
下篇预告:第319篇 — ChatOps,将介绍如何通过聊天机器人(WeChat/Slack/钉钉)执行网络运维操作,实现"聊天即运维"。