// RHEL / CentOS 9 NetworkManager keyfile 风格网络配置 // 覆盖: CentOS Stream 9, RHEL 9, Rocky Linux 9, AlmaLinux 9 // 配置文件位于 /etc/NetworkManager/system-connections/.nmconnection // 说明: RHEL 9 / CentOS Stream 9 已弃用 network-scripts (ifcfg-*),必须使用 NM keyfile export const networkRhelNmKeyfileSchema = { id: 'network-rhel-nmkeyfile', name: 'RHEL/CentOS 9 NM', icon: 'Connection', category: '网络配置', description: 'RHEL/CentOS 9 NetworkManager keyfile(含 Bond/Bridge/VLAN)', format: 'ini', fileName: 'connection.nmconnection', groups: [ { title: '系统版本', fields: [ { key: 'osVersion', label: '操作系统版本', type: 'select', options: [ { label: 'CentOS Stream 9', value: 'centos-stream9' }, { label: 'RHEL 9', value: 'rhel9' }, { label: 'Rocky Linux 9', value: 'rocky9' }, { label: 'AlmaLinux 9', value: 'alma9' }, ], default: 'centos-stream9', tip: '语法相同,仅影响注释与说明', }, ], }, { title: '配置类型', fields: [ { key: 'configType', label: '配置类型', type: 'select', options: [ { label: '单网卡 - 静态 IP', value: 'static' }, { label: '单网卡 - DHCP', value: 'dhcp' }, { label: 'Bond - 链路聚合', value: 'bond' }, { label: 'Bridge - 桥接', value: 'bridge' }, { label: 'VLAN - 子接口', value: 'vlan' }, ], default: 'static', }, ], }, { title: '主接口配置', fields: [ { key: 'interface', label: '主接口名', type: 'text', default: 'eth0', tip: 'nmconnection 文件名建议与接口名一致', }, { key: 'ipaddr', label: 'IP 地址', type: 'text', default: '192.168.1.10', dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] }, }, { key: 'prefix', label: '子网前缀 (CIDR)', type: 'number', min: 1, max: 32, default: 24, dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] }, }, { key: 'gateway', label: '默认网关', type: 'text', default: '192.168.1.1', dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] }, }, { key: 'dns', label: 'DNS (逗号分隔)', type: 'text', default: '8.8.8.8,8.8.4.4', dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] }, }, { key: 'routeMetric', label: '路由 Metric', type: 'number', min: 0, max: 9999, default: 0, tip: '默认路由优先级,值越小优先级越高;0 表示不输出', dependsOn: { key: 'configType', valueIn: ['static', 'bond', 'bridge', 'vlan'] }, }, { key: 'autoconnect', label: '开机自动启用', type: 'switch', default: true, }, { key: 'ipv6Method', label: 'IPv6 方法', type: 'select', options: [ { label: 'disabled - 禁用', value: 'disabled' }, { label: 'auto - 自动', value: 'auto' }, { label: 'ignore - 忽略', value: 'ignore' }, ], default: 'disabled', }, ], }, { title: 'Bond 配置', fields: [ { key: 'bondName', label: 'Bond 接口名', type: 'text', default: 'bond0', dependsOn: { key: 'configType', value: 'bond' }, }, { key: 'bondMode', label: 'Bond 模式', type: 'select', options: [ { label: 'balance-rr (0)', value: 'balance-rr' }, { label: 'active-backup (1)', value: 'active-backup' }, { label: 'balance-xor (2)', value: 'balance-xor' }, { label: 'broadcast (3)', value: 'broadcast' }, { label: '802.3ad (4 LACP)', value: '802.3ad' }, { label: 'balance-tlb (5)', value: 'balance-tlb' }, { label: 'balance-alb (6)', value: 'balance-alb' }, ], default: '802.3ad', tip: 'mode 4 需要交换机端开启 LACP', dependsOn: { key: 'configType', value: 'bond' }, }, { key: 'miimon', label: 'MII 监控间隔 (ms)', type: 'number', min: 0, max: 10000, default: 100, dependsOn: { key: 'configType', value: 'bond' }, }, { key: 'lacpRate', label: 'LACP 速率', type: 'select', options: [ { label: 'slow (30s)', value: 'slow' }, { label: 'fast (1s)', value: 'fast' }, ], default: 'fast', tip: '仅 802.3ad 模式生效', dependsOn: { key: 'configType', value: 'bond' }, }, { key: 'bondSlaves', label: '成员网卡 (逗号分隔)', type: 'text', default: 'eth0,eth1', dependsOn: { key: 'configType', value: 'bond' }, }, ], }, { title: 'Bridge 配置', fields: [ { key: 'bridgeName', label: 'Bridge 接口名', type: 'text', default: 'br0', dependsOn: { key: 'configType', value: 'bridge' }, }, { key: 'bridgePorts', label: '桥接成员 (逗号分隔)', type: 'text', default: 'eth0', dependsOn: { key: 'configType', value: 'bridge' }, }, { key: 'bridgeStp', label: '启用 STP', type: 'switch', default: false, dependsOn: { key: 'configType', value: 'bridge' }, }, { key: 'bridgeHelloTime', label: 'STP Hello 时间 (秒)', type: 'number', min: 1, max: 10, default: 2, dependsOn: { key: 'configType', value: 'bridge' }, }, ], }, { title: 'VLAN 配置', fields: [ { key: 'vlanParent', label: '父接口', type: 'text', default: 'eth0', dependsOn: { key: 'configType', value: 'vlan' }, }, { key: 'vlanId', label: 'VLAN ID', type: 'number', min: 1, max: 4094, default: 100, dependsOn: { key: 'configType', value: 'vlan' }, }, ], }, ], } function genKeyfile(sections, headerComment) { const lines = [] if (headerComment) { lines.push(headerComment) } for (const [section, kv] of Object.entries(sections)) { lines.push(`[${section}]`) for (const [k, v] of Object.entries(kv)) { if (v === undefined || v === null || v === '') continue lines.push(`${k}=${v}`) } lines.push('') } return lines.join('\n') } export function generateNetworkRhelNmKeyfile(config) { const out = [] const ts = new Date().toLocaleString('zh-CN') out.push(`# =========================================================`) out.push(`# RHEL/CentOS 9 NetworkManager keyfile 配置 - 由 ConfTemplate 生成`) out.push(`# 生成时间: ${ts}`) out.push(`# 目标目录: /etc/NetworkManager/system-connections/`) out.push(`# 配置类型: ${config.configType}`) out.push(`# =========================================================`) out.push(``) out.push(`# 应用步骤:`) out.push(`# 1. 将下面每个 .nmconnection 内容保存到对应文件`) out.push(`# 2. 设置权限: chmod 600 .nmconnection`) out.push(`# 3. 执行: nmcli connection reload`) out.push(`# nmcli connection up `) out.push(``) out.push(`# keyfile 必须有 600 权限,否则 NetworkManager 会忽略它`) out.push(`# -----------------------------------------------------------`) const autoStr = config.autoconnect ? 'true' : 'false' const dnsList = String(config.dns || '').split(',').map((s) => s.trim()).filter(Boolean).join(';') const dnsSuffix = dnsList ? `${dnsList};` : '' if (config.configType === 'static' || config.configType === 'dhcp') { const sections = { connection: { id: config.interface, type: 'ethernet', 'interface-name': config.interface, autoconnect: autoStr, }, ipv4: {}, ipv6: { method: config.ipv6Method }, } if (config.configType === 'dhcp') { sections.ipv4.method = 'auto' } else { sections.ipv4.method = 'manual' sections.ipv4.addresses = `${config.ipaddr}/${config.prefix}` sections.ipv4.gateway = config.gateway if (dnsSuffix) sections.ipv4.dns = dnsSuffix if (config.routeMetric) sections.ipv4['route-metric'] = String(config.routeMetric) } out.push(``) out.push(`# >>> 文件: ${config.interface}.nmconnection`) out.push(`# >>> 接口 ${config.interface} (${config.configType === 'dhcp' ? 'DHCP' : '静态'})`) out.push(genKeyfile(sections)) } if (config.configType === 'bond') { // bond 主接口 const bondSections = { connection: { id: config.bondName, type: 'bond', 'interface-name': config.bondName, autoconnect: autoStr, }, bond: { miimon: String(config.miimon), mode: config.bondMode, }, ipv4: { method: 'manual', addresses: `${config.ipaddr}/${config.prefix}`, gateway: config.gateway, dns: dnsSuffix, }, ipv6: { method: config.ipv6Method }, } if (config.routeMetric) bondSections.ipv4['route-metric'] = String(config.routeMetric) if (config.bondMode === '802.3ad') { bondSections.bond['lacp_rate'] = config.lacpRate } out.push(``) out.push(`# >>> 文件: ${config.bondName}.nmconnection`) out.push(`# >>> Bond 主接口 ${config.bondName} (mode=${config.bondMode})`) out.push(genKeyfile(bondSections)) // 成员接口 const slaves = String(config.bondSlaves || '').split(',').map((s) => s.trim()).filter(Boolean) for (const slv of slaves) { const slaveSections = { connection: { id: slv, type: 'ethernet', 'interface-name': slv, master: config.bondName, 'slave-type': 'bond', autoconnect: autoStr, }, ipv4: { method: 'disabled' }, ipv6: { method: config.ipv6Method }, } out.push(`# >>> 文件: ${slv}.nmconnection`) out.push(`# >>> Bond 成员 ${slv} → ${config.bondName}`) out.push(genKeyfile(slaveSections)) } out.push(`# 提示: mode=802.3ad 需要在交换机端开启 LACP`) } if (config.configType === 'bridge') { // bridge 主接口 const brSections = { connection: { id: config.bridgeName, type: 'bridge', 'interface-name': config.bridgeName, autoconnect: autoStr, }, bridge: { stp: config.bridgeStp ? 'yes' : 'no', 'hello-time': String(config.bridgeHelloTime), }, ipv4: { method: 'manual', addresses: `${config.ipaddr}/${config.prefix}`, gateway: config.gateway, dns: dnsSuffix, }, ipv6: { method: config.ipv6Method }, } if (config.routeMetric) brSections.ipv4['route-metric'] = String(config.routeMetric) out.push(``) out.push(`# >>> 文件: ${config.bridgeName}.nmconnection`) out.push(`# >>> Bridge ${config.bridgeName} (STP=${config.bridgeStp ? 'on' : 'off'})`) out.push(genKeyfile(brSections)) // 成员接口 const ports = String(config.bridgePorts || '').split(',').map((s) => s.trim()).filter(Boolean) for (const p of ports) { const portSections = { connection: { id: p, type: 'ethernet', 'interface-name': p, master: config.bridgeName, 'slave-type': 'bridge', autoconnect: autoStr, }, ipv4: { method: 'disabled' }, ipv6: { method: config.ipv6Method }, } out.push(`# >>> 文件: ${p}.nmconnection`) out.push(`# >>> Bridge 成员 ${p} → ${config.bridgeName}`) out.push(genKeyfile(portSections)) } } if (config.configType === 'vlan') { const subName = `${config.vlanParent}.${config.vlanId}` const vlanSections = { connection: { id: subName, type: 'vlan', 'interface-name': subName, autoconnect: autoStr, }, vlan: { parent: config.vlanParent, id: String(config.vlanId), }, ipv4: { method: 'manual', addresses: `${config.ipaddr}/${config.prefix}`, gateway: config.gateway, dns: dnsSuffix, }, ipv6: { method: config.ipv6Method }, } if (config.routeMetric) vlanSections.ipv4['route-metric'] = String(config.routeMetric) out.push(``) out.push(`# >>> 文件: ${subName}.nmconnection`) out.push(`# >>> VLAN 子接口 ${subName} (VLAN ID=${config.vlanId})`) out.push(genKeyfile(vlanSections)) } return out.join('\n') }