作者从事煤矿信息化工作,常年和井下环网、切片网络、设备割接打交道。本文记录如何用 WorkBuddy 从零搭一个「切片网络 IP 规划工具」,把 Excel 模板里的 IPv4/IPv6 Loopback、设备互联地址、SRv6 Locator 自动生成并回填。
一次典型的切片网络(SPN / 切片分组网)项目交付,网络规划阶段要交出一整套《切片网络规划模板.xlsx》。其中地址规划部分大概长这样:
表号 | 内容 | 手工做法 |
|---|---|---|
表 2.3 | 站点设备清单 | 人工录入,基线数据 |
表 2.4 | IPv4 地址规划 | 每台设备一个 /32 Loopback,手敲 |
表 2.5 | IPv6 地址规划 | 每台设备一个 /128 Loopback,手敲 |
表 2.6 | 设备互联地址 | 按 /30 分段,首尾可用地址分给两端 |
表 2.7 | SRv6 Locator 规划 | 每个节点一段 /64,还要算 End SID |
一个中等规模的矿井,核心 + 汇聚 + 接入设备 30~80 台,链路几十条。手工做的真实情况是:
所以这个项目最值得自动化的,恰恰是看起来最"没技术含量"的填表。
我把整个工具的输入收敛成一句话:只认表 2.3,其余全推导。
表2.3 站点设备清单(站点/设备名称/设备角色/接入环/区域)
│
├─→ 拓扑推导:环内互联 → 汇聚上联核心 → 核心间全互联
│
├─→ 地址分配:Loopback v4 / Loopback v6 / 互联 /30 / SRv6 Locator
│
├─→ 冲突校验:重复检测 + 越界检测 + 地址池容量检测
│
└─→ 回填输出:2.4 / 2.5 / 2.6 / 2.7 + 校验报告这么设计的好处是:站前设计一变,改表 2.3 再跑一次脚本就行,不用碰任何公式。
PLAN = {
"area_id": 10, # 区域编号,参与拼装 IPv4 第二段与 IPv6 区域位
"loopback_v4": "10.10.0.0/16", # IPv4 Loopback 地址池
"loopback_v4_start": 1, # 起始主机位
"loopback_v6": "fd00:a::/32", # IPv6 Loopback 地址池
"link_v4": "10.10.200.0/24", # 设备互联地址池,按 /30 划分
"srv6_locator": "2001:db8:a::/48", # SRv6 Locator 前缀
"uplink_mask": 30, # 互联链路掩码
}换项目只改这个字典,其余逻辑不动。
python -m venv .venv
.venv\Scripts\activate # Linux/macOS 用 source .venv/bin/activate
pip install openpyxl内网环境小贴士:煤矿项目现场常常没有外网。在有网的机器上
pip download openpyxl -d ./pkgs把 wheel 拷进去,然后pip install --no-index --find-links=./pkgs openpyxl离线安装,屡试不爽。
规划模板每次版本都不一样,列名可能叫"设备名称"也可能叫"网元名称","设备角色"也可能写成"网络层次"。所以我没用固定列号,而是做了别名匹配 + 长名优先:
HEADER_ALIAS = {
"site": ["站点", "站点名称", "机房", "所属站点"],
"name": ["设备名称", "网元名称", "设备", "NE名称"],
"role": ["设备角色", "角色", "层级", "网络层次"],
"ring": ["接入环", "所属环", "环", "环网"],
"area": ["区域", "区域编号", "所属区域"],
}
def _match_header(cell_value):
"""按别名长度降序匹配,避免「设备名称」被短别名「设备」抢先命中"""
v = re.sub(r"\s+", "", str(cell_value)) if cell_value is not None else ""
flat = [(a, std) for std, aliases in HEADER_ALIAS.items() for a in aliases]
for alias, std in sorted(flat, key=lambda x: -len(x[0])):
if alias in v:
return std
return None这里是我踩的第一个坑:一开始按字典顺序遍历别名,"设备角色" 会被 "设备" 抢先匹配成 name,结果整列角色丢失,所有设备都被当成接入层,拓扑直接错。改成按别名长度降序匹配后解决。
不需要画拓扑图,角色 + 环号就够了:
def build_topology(devices):
links, seen = [], set()
rings = defaultdict(list)
for d in devices:
if d.ring:
rings[d.ring].append(d)
for ring, devs in rings.items(): # 环内互联,3 个点以上闭合
devs = sorted(devs, key=lambda x: x.name)
pairs = list(zip(devs, devs[1:]))
if len(devs) >= 3:
pairs.append((devs[-1], devs[0]))
for a, b in pairs:
key = frozenset((a.name, b.name))
if key not in seen:
seen.add(key)
links.append(Link("接入环", ring, a, b))
cores = [d for d in devices if d.role == "核心"]
aggs = [d for d in devices if d.role == "汇聚"]
for ag in aggs: # 汇聚双上联核心
for c in cores:
key = frozenset((ag.name, c.name))
if key not in seen:
seen.add(key)
links.append(Link("上联", f"{ag.ring or ag.site}-上联", ag, c))
for a, b in combinations(sorted(cores, key=lambda x: x.name), 2):
links.append(Link("核心互联", "核心", a, b))
return links接口编号按设备已用数量顺序分配(GE0/0/1、GE0/0/2…),输出时直接带上,割接时照表配就行。
Python 标准库 ipaddress 是这类工具的核心武器,加减、子网划分、越界判断全包了:
def allocate(devices, links):
lo4_pool = ipaddress.ip_network(PLAN["loopback_v4"])
lo6_net = ipaddress.ip_network(PLAN["loopback_v6"])
link_pool = ipaddress.ip_network(PLAN["link_v4"])
loc_net = ipaddress.ip_network(PLAN["srv6_locator"])
for i, d in enumerate(devices, start=PLAN["loopback_v4_start"]):
d.idx = i
d.lo_v4 = f"{lo4_pool.network_address + i}/32"
d.lo_v6 = f"{lo6_net.network_address + i}/128"
# Locator:/48 前缀 + 16bit 节点号 → /64;End SID 固定 function ::1
d.locator = f"{loc_net.network_address + (i << 64)}/64"
d.end_sid = str(ipaddress.ip_network(d.locator, strict=False).network_address + 1)
subnets = list(link_pool.subnets(new_prefix=PLAN["uplink_mask"]))
if len(links) > len(subnets):
raise RuntimeError(f"互联地址池 {link_pool} 仅可划分 {len(subnets)} 段 /30,实际需要 {len(links)} 段")
for n, lk in enumerate(links):
net = subnets[n]
hosts = list(net.hosts()) # /30 只有两个可用地址,恰好一端一个
lk.net, lk.a_ip, lk.b_ip = str(net), f"{hosts[0]}/30", f"{hosts[1]}/30"
lk.a_if, lk.b_if = lk.a.next_iface(), lk.b.next_iface()两个关键点:
i << 64。手算十六进制是事故高发区。net.hosts() 正好返回这两个,hosts[0] 给 A 端、hosts[1] 给 B 端,永远不会串段。def write_sheet(wb, title, headers, rows, widths=None):
ws = wb.create_sheet(title)
ws.append(headers)
for c in ws[1]:
c.fill, c.font = HEAD_FILL, HEAD_FONT
c.alignment, c.border = Alignment(horizontal="center"), BORDER
for r in rows:
ws.append(r)
for i, w in enumerate(widths or [16] * len(headers), start=1):
ws.column_dimensions[get_column_letter(i)].width = w
ws.freeze_panes = "A2"
return ws校验部分做的是规划交付前必查的三件事:
def validate(devices, links):
issues = []
# 1. Loopback / Locator 全局唯一性
# 2. 地址是否落在预设地址池内(越界检测)
# 3. 每条链路的两端地址是否真属于该 /30 子网
return issues$ python ip_plan.py --demo
[demo] 已生成示例基线:表2.3_站点设备清单_示例.xlsx
设备 10 台 / 链路 13 条
校验:全部通过
已输出:切片网络规划_已填充.xlsx生成的 2.4 表(节选):
序号 | 站点 | 设备名称 | 设备角色 | 所属环 | IPv4 Loopback |
|---|---|---|---|---|---|
1 | 地面中心站 | Core-01 | 核心 | — | 10.10.0.1 |
2 | 地面中心站 | Core-02 | 核心 | — | 10.10.0.2 |
3 | 中央变电所 | Agg-01 | 汇聚 | 中央变电所环 | 10.10.0.3 |
4 | 风井场地 | Agg-02 | 汇聚 | 风井环 | 10.10.0.4 |
生成的 2.6 设备互联地址(节选):
链路类型 | 环 | A端设备 | A端接口 | A端IP | B端设备 | B端接口 | B端IP | 子网 |
|---|---|---|---|---|---|---|---|---|
接入环 | 中央变电所环 | Acc-01 | GE0/0/1 | 10.10.200.1/30 | Acc-02 | GE0/0/1 | 10.10.200.2/30 | 10.10.200.0/30 |
接入环 | 中央变电所环 | Acc-02 | GE0/0/2 | 10.10.200.5/30 | Acc-03 | GE0/0/1 | 10.10.200.6/30 | 10.10.200.4/30 |
接入环 | 中央变电所环 | Acc-03 | GE0/0/2 | 10.10.200.9/30 | Agg-01 | GE0/0/1 | 10.10.200.10/30 | 10.10.200.8/30 |
接入环 | 中央变电所环 | Agg-01 | GE0/0/2 | 10.10.200.13/30 | Acc-01 | GE0/0/2 | 10.10.200.14/30 | 10.10.200.12/30 |
生成的 2.7 SRv6 Locator(节选):
设备名称 | 设备角色 | SRv6 Locator | 掩码 | End SID |
|---|---|---|---|---|
Core-01 | 核心 | 2001:db8:a:1:: | /64 | 2001:db8:a:1::1 |
Core-02 | 核心 | 2001:db8:a:2:: | /64 | 2001:db8:a:2::1 |
Agg-01 | 汇聚 | 2001:db8:a:3:: | /64 | 2001:db8:a:3::1 |
坑 1:表头别名抢占匹配。 已在上文说明,"设备" 会抢走 "设备角色",必须长名优先。这个坑的隐蔽之处在于——它不报错,只是默默把角色识别错,输出看起来"很正常"。
坑 2:load_workbook 默认读公式不读值。 如果表 2.3 里有公式(比如站点名用 VLOOKUP 拼出来),必须加 data_only=True,否则读到的全是 =VLOOKUP(...) 字符串。
坑 3:合并单元格读不到值。 合并区域只有左上角单元格有值,其余是 None。表 2.3 若有合并的站点名,需要做前向填充(ffill)。
坑 4:地址池容量要在分配前算。 /24 的互联池最多划分 64 段 /30,链路一多就溢出。必须提前 raise,否则会静默丢链路。
坑 5:分配顺序要稳定。 设备按 核心 → 汇聚 → 接入 + 环名 + 设备名 排序后再编号,否则同一份清单两次运行会得到两套地址,交付件无法复现。
这个工具是在 WorkBuddy 里对话式迭代出来的,我的实际用法是:
指标 | 手工 | 工具 |
|---|---|---|
80 台设备地址规划 | 6~8 小时 | 约 30 秒(含校验) |
插入设备后重排 | 重做一遍 | 改表 2.3 重跑 |
重号/越界错误 | 割接时才发现 | 生成时直接拦截 |
交付一致性 | 依赖个人习惯 | 模板统一,可复现 |
更实在的收益是:地址规划不再是割接前夜的风险项。脚本跑完那一刻,地址冲突就已经排除了。
interface + ip address + segment-routing ipv6 locator),直接粘到割接脚本;完整代码:ip_plan.py(单文件,约 300 行,仅依赖 openpyxl)。
# 跑通示例
python ip_plan.py --demo
# 用真实模板
python ip_plan.py -i 切片网络规划模板.xlsx -o 切片网络规划_已填充.xlsx如果你也在做煤矿或其他工业场景的网络规划,欢迎交流地址池划分和切片隔离的做法。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。