第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/钉钉)执行网络运维操作,实现"聊天即运维"。