C13-OpenStack 網路服務 2:OVN 架構、Provider 與 Self-Service 網路
0. 環境與準備
0.1 本課環境
| 主機 | 角色 | 管理網 IP |
|---|---|---|
| jmp001 | 學員操作機(OpenStack CLI),接在外部網段 192.168.100.0/24 | 10.127.0.3 |
| ctl001 / ctl002 / ctl003 | 控制節點:OVN Central(ovn_nb_db、ovn_northd、ovn_sb_db),同時是 Gateway(承載 cr-lrp) | 10.127.0.4 / .14 / .15 |
| scp001 | 運算節點(ovn_controller、openvswitch_vswitchd),c13-vm-a 在這台 | 10.127.0.7 |
| scp002 | 運算節點(ovn_controller、openvswitch_vswitchd),c13-vm-b 在這台 | 10.127.0.18 |
- 認證檔:jmp001 的
/home/ubuntu/creds/stuNN-openrc.sh(NN 為學員編號 01–20) - 本課只做觀察:所有資源已由講師事先建立,請不要新增、修改或刪除任何東西
- OVN / OVS 的指令都在容器裡:
ovn-nbctl在ovn_nb_db、ovn-sbctl在ovn_sb_db、ovn-trace在ovn_northd、ovs-vsctl在每台節點的openvswitch_vswitchd
講師建立的範例情境(對應投影片 p.10–11、p.29):
| 資源 | 名稱 | 說明 |
|---|---|---|
| Network / Subnet | c13-private / 192.168.13.0/24 | Self-Service 網路(geneve) |
| Router | c13-router | 接 c13-private,對外閘道為 public1,SNAT 開啟 |
| Security Group | c13-sg | ingress 只放行 ICMP 與 TCP 22 |
| VM | c13-vm-a | 在 scp001,綁 Floating IP(1:1 NAT) |
| VM | c13-vm-b | 在 scp002,沒有 Floating IP(走 Router 的 SNAT) |
0.2 連線方式
所有指令都在 jmp001 執行。需要看 OVN 或 OVS 時,用 ssh <節點 IP> "sudo docker exec <容器> <指令>" 送到該節點,這樣 jmp001 上設好的變數(例如 $NET)可以直接帶進去。
⚠️ 規則
- 你使用的是和講師相同的管理員權限,本課只執行查詢指令
- 預期輸出是示意,UUID、MAC、IP 若講師重建情境會不同;請以你自己 0.3 查到的值為準
ovn-trace偶爾會先印出clustered database server is not cluster leader; trying another server,這是它在找 SB DB 的 leader,屬正常訊息
0.3 準備:把要追蹤的資源 ID 存成變數
# [jmp001]
export NN=01 # ← 換成你的學員編號(01–20)
source ~/venv/bin/activate
source ~/creds/stu${NN}-openrc.sh
NET=$(openstack network show c13-private -f value -c id)
ROUTER=$(openstack router show c13-router -f value -c id)
SG=$(openstack security group show c13-sg -f value -c id)
PORT_A=$(openstack port list --server c13-vm-a -f value -c ID)
PORT_B=$(openstack port list --server c13-vm-b -f value -c ID)
MAC_A=$(openstack port list --server c13-vm-a -f value -c "MAC Address")
MAC_B=$(openstack port list --server c13-vm-b -f value -c "MAC Address")
IP_A=$(openstack port list --server c13-vm-a -f value -c "Fixed IP Addresses" | cut -d"'" -f8)
IP_B=$(openstack port list --server c13-vm-b -f value -c "Fixed IP Addresses" | cut -d"'" -f8)
GW_MAC=$(openstack port list --router c13-router --device-owner network:router_interface -f value -c "MAC Address")
GW_PORT=$(openstack port list --router c13-router --device-owner network:router_gateway -f value -c ID)
echo "NET=$NET ROUTER=$ROUTER"
echo "A: $PORT_A $MAC_A $IP_A"
echo "B: $PORT_B $MAC_B $IP_B"
echo "GW_MAC=$GW_MAC GW_PORT=$GW_PORT"預期輸出:
NET=cffbde07-14dd-4707-a48b-5149515de3a5 ROUTER=aa7271d2-b52c-408a-b397-5a72036746de
A: 1021e30d-24cf-4a41-8403-85852c7119e4 fa:16:3e:73:0f:22 192.168.13.252
B: f7221753-088b-4710-9136-623f2c565c6f fa:16:3e:e8:53:d8 192.168.13.82
GW_MAC=fa:16:3e:61:02:68 GW_PORT=5c6c01e8-0f46-418c-91da-260f1e488cd6| 變數 | 內容 | 後面用在哪裡 |
|---|---|---|
NET / ROUTER | Network、Router 的 Neutron ID | OVN 用 neutron-<ID> 命名對應的 Switch、Router |
PORT_A / PORT_B | 兩台 VM 網卡的 Neutron Port ID | 在 OVN、OVS 中找到同一個網卡 |
MAC_A / IP_A 等 | VM 網卡的 MAC 與 IP | F 節模擬封包時當作來源 |
GW_MAC | Router Interface(192.168.13.1)的 MAC | F 節模擬「送往閘道」的封包 |
GW_PORT | Router Gateway Port(對外那一側)的 ID | C1 查它落在哪台節點 |
- 四行都要有值才往下做;有空白代表前面某一行沒有執行成功
cut -d"'" -f8:Fixed IP Addresses的格式是[{'subnet_id': '…', 'ip_address': '192.168.13.252'}],以單引號切開後第 8 段就是 IP- 換終端機或重新 SSH 後,這一段要重新執行一次
Part A:API 層:先從 Neutron 資源開始(p.8、10)
# [jmp001]
openstack server list --name ^c13- --long -c Name -c Networks -c Host
openstack router show c13-router -c interfaces_info -c external_gateway_info -f yaml預期輸出:
+----------+---------------------------------------------+---------------------+
| Name | Networks | Host |
+----------+---------------------------------------------+---------------------+
| c13-vm-b | c13-private=192.168.13.82 | scp002-6164930-iaas |
| c13-vm-a | c13-private=192.168.100.156, 192.168.13.252 | scp001-6161072-iaas |
+----------+---------------------------------------------+---------------------+
external_gateway_info:
enable_snat: true
external_fixed_ips:
- ip_address: 192.168.100.185
subnet_id: b7dabca1-dc52-47dd-b3ba-22e03e7f2851
network_id: 351d82a1-2a0e-42d5-a428-3c7baa0be512
interfaces_info:
- ip_address: 192.168.13.1
port_id: 362f0f36-a4e0-48c5-a4f9-e663a72c8ab3
subnet_id: 27dce4dc-97e0-4134-b1ea-9a85650f7f01怎麼讀這段輸出
| 輸出 | 意思 |
|---|---|
Host 欄 | VM 實際跑在哪一台運算節點;D 節要登入的就是這台 |
vm-a 的 Networks 有兩個位址 | 192.168.13.252 是 VM 自己的 fixed IP,192.168.100.156 是 Floating IP。這個欄位不區分類型(C12 學過) |
external_gateway_info.enable_snat: true | Router 對外閘道已設定,SNAT 開著 |
external_fixed_ips … 192.168.100.185 | Router 在外部網路上的位址;沒有 FIP 的 VM 出外網都用這個位址 |
interfaces_info … 192.168.13.1 | Router 接在 c13-private 這一側的位址,也就是 VM 的預設閘道 |
到這裡為止都是 Neutron 的視角。下面的表是本課的「尋寶清單」:同一個資源,接下來在每一層都要找到它。
| Neutron 資源 | 值 | 後面在哪裡出現 |
|---|---|---|
| Network c13-private | $NET | B:Logical Switch neutron-$NET |
| Router c13-router | $ROUTER | B:Logical Router neutron-$ROUTER |
| vm-a 的 Port | $PORT_A,192.168.13.252,FIP 192.168.100.156 | B:LSP、NAT;C:Port_Binding;D:tap |
| vm-b 的 Port | $PORT_B,192.168.13.82 | B:LSP;C:Port_Binding |
| Router Interface | 362f0f36-…,192.168.13.1 | B:type: router 的 port 與 lrp-362f0f36-… |
| Router Gateway Port | $GW_PORT,192.168.100.185 | B:lrp-5c6c01e8-…、snat;C:cr-lrp-5c6c01e8-… |
Part B:第一層:NB DB,Neutron 的意圖(p.5、11)
ML2/OVN driver 把 Neutron 資源寫成 Northbound DB 裡的邏輯物件。這一層只描述「網路應該長什麼樣」,還沒有任何一台節點的資訊。
B1. Logical Switch:Network 變成了什麼
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl show neutron-$NET"預期輸出:
switch 987e880a-b8c4-4e91-bf21-3d20c6c674de (neutron-cffbde07-14dd-4707-a48b-5149515de3a5) (aka c13-private)
port 362f0f36-a4e0-48c5-a4f9-e663a72c8ab3
type: router
router-port: lrp-362f0f36-a4e0-48c5-a4f9-e663a72c8ab3
port f7221753-088b-4710-9136-623f2c565c6f
addresses: ["fa:16:3e:e8:53:d8 192.168.13.82"]
port 1021e30d-24cf-4a41-8403-85852c7119e4
addresses: ["fa:16:3e:73:0f:22 192.168.13.252"]
port 7dc732a2-d036-42d0-a6f5-6e27d3188e8b
type: localport
addresses: ["fa:16:3e:50:24:fd 192.168.13.2"]怎麼讀這段輸出
| 輸出 | 意思 |
|---|---|
switch 987e880a-… | OVN 自己替這個 Logical Switch 編的 UUID,和 Neutron 無關,排查時用不到 |
(neutron-cffbde07-…) | Switch 的名稱,就是 neutron- 加上 Neutron Network ID($NET)。用 Neutron ID 搜尋最可靠 |
(aka c13-private) | 給人看的別名,就是 openstack 指令看到的網路名稱 |
port 1021e30d-… | Logical Switch Port(LSP)。名稱就是 Neutron Port ID,這一個是 vm-a($PORT_A) |
addresses: ["MAC IP"] | 這個埠允許使用的 MAC 與 IP。OVN 依此做 port security:VM 偽造其他來源位址的封包會被丟掉 |
type: router + router-port: lrp-362f0f36-… | 這個 LSP 不是 VM,而是 Router Interface 在 Switch 這一側的接點,另一端是 Router 上的 lrp-362f0f36-…(B2 會看到) |
type: localport(192.168.13.2) | 每台節點本機各有一份的 metadata 埠,VM 開機時從這裡取得 metadata。C12 看到的 network:distributed 就是它 |
一句話總結:Network 變成 Logical Switch,Port 變成 Logical Switch Port,名稱都直接用 Neutron ID。
B2. Logical Router 與 NAT:Router 和 FIP 變成了什麼
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl show neutron-$ROUTER"
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl lr-route-list neutron-$ROUTER"預期輸出:
router 0df3c906-4c39-43ed-9423-57cede0ecb10 (neutron-aa7271d2-b52c-408a-b397-5a72036746de) (aka c13-router)
port lrp-5c6c01e8-0f46-418c-91da-260f1e488cd6
mac: "fa:16:3e:f1:b7:52"
ipv6-lla: "fe80::f816:3eff:fef1:b752"
networks: ["192.168.100.185/24"]
gateway chassis: [ctl001-6161060-iaas ctl002-6161063-iaas ctl003-6161069-iaas]
port lrp-362f0f36-a4e0-48c5-a4f9-e663a72c8ab3
mac: "fa:16:3e:61:02:68"
ipv6-lla: "fe80::f816:3eff:fe61:268"
networks: ["192.168.13.1/24"]
nat 0248d8a8-f9d7-4f40-b8b3-1d7078d2a0e7
external ip: "192.168.100.185"
logical ip: "192.168.13.0/24"
type: "snat"
nat f91483e9-5c7a-4e06-a9a4-cf793071c5f5
external ip: "192.168.100.156"
logical ip: "192.168.13.252"
type: "dnat_and_snat"
IPv4 Routes
Route Table <main>:
0.0.0.0/0 192.168.100.1 dst-ip怎麼讀這段輸出
| 輸出 | 意思 | 對應的 Neutron 資源 |
|---|---|---|
port lrp-362f0f36-…,networks: ["192.168.13.1/24"] | Logical Router Port(LRP),接內部子網 | Router Interface,mac 就是 $GW_MAC |
port lrp-5c6c01e8-…,networks: ["192.168.100.185/24"] | LRP,接外部網路 | Router Gateway Port($GW_PORT) |
mac | 這個 Router 埠的 MAC。F 節會看到封包經過 Router 後,來源 MAC 被換成它 | — |
ipv6-lla | 自動產生的 IPv6 link-local 位址,本課不使用 | — |
gateway chassis: [ctl001 ctl002 ctl003] | 這個對外埠可以落在哪幾台節點。這是候選名單,列出的順序不代表優先順序;實際落點在 C1 確認 | — |
nat … type: "dnat_and_snat",.156 ↔ .252 | 1:1 NAT:進來時把目的位址 .156 換成 .252(DNAT),出去時把來源 .252 換成 .156(SNAT) | vm-a 的 Floating IP |
nat … type: "snat",192.168.13.0/24 → .185 | 多對一 NAT:整個子網出去時,來源都換成 .185 | Router 的 SNAT(enable_snat: true) |
0.0.0.0/0 192.168.100.1 | Router 的預設路由:不認識的目的地都交給 192.168.100.1(外部的實體路由器) | 由 public1-subnet 的 gateway 自動產生 |
B3. 表格形式的 NAT 清單
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl lr-nat-list neutron-$ROUTER"預期輸出:
TYPE GATEWAY_PORT MATCH EXTERNAL_IP EXTERNAL_PORT LOGICAL_IP EXTERNAL_MAC LOGICAL_PORT
dnat_and_snat lrp-5c6c01e8-0f46- 192.168.100.156 192.168.13.252
snat 192.168.100.185 192.168.13.0/24怎麼讀這段輸出
| 欄位 | 意思 |
|---|---|
TYPE | dnat_and_snat = Floating IP;snat = Router 的 SNAT |
EXTERNAL_IP / LOGICAL_IP | 外面看到的位址 / 裡面真正的位址 |
GATEWAY_PORT | 這條 NAT 在哪個 Router 埠上做(欄寬有限,名稱被截斷) |
EXTERNAL_MAC、LOGICAL_PORT 空白 | FIP 是集中式:NAT 在 Gateway 節點上做,所以 vm-a 的對外流量要先經 Tunnel 送到 Gateway(ctl)。若這兩欄有值,則是分散式 FIP,NAT 直接在 VM 所在節點做 |
p.33 的判斷法:有 snat 表示整個 Subnet 的出口已成立;有 dnat_and_snat 表示某個 Port 另外掛了 Floating IP。兩者都沒有,問題在 Router 兩側的接線。
B4. Security Group 變成 Port Group + ACL
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl acl-list pg_${SG//-/_}"預期輸出:
from-lport 1002 (inport == @pg_dd92c260_5907_4b53_b3de_235c2a3ba09a && ip4) allow-related
from-lport 1002 (inport == @pg_dd92c260_5907_4b53_b3de_235c2a3ba09a && ip6) allow-related
to-lport 1002 (outport == @pg_dd92c260_5907_4b53_b3de_235c2a3ba09a && ip4 && ip4.src == 0.0.0.0/0 && icmp4) allow-related
to-lport 1002 (outport == @pg_dd92c260_5907_4b53_b3de_235c2a3ba09a && ip4 && ip4.src == 0.0.0.0/0 && tcp && tcp.dst == 22) allow-related${SG//-/_} 是 bash 的字串取代:把 SG ID 裡的 - 換成 _,因為 Port Group 的名稱是 pg_ 加上 SG ID,而名稱裡不能有 -。
怎麼讀一條 ACL:以第三條為例,可以拆成四段
to-lport 1002 (outport == @pg_3d64… && ip4 && ip4.src == 0.0.0.0/0 && icmp4) allow-related
──────── ──── ───────────────────────────────────────────────────────────── ─────────────
① 方向 ② 優先權 ③ 比對條件 ④ 動作| 段 | 意思 |
|---|---|
| ① 方向 | from-lport:封包從 VM 出來(egress);to-lport:封包要進入 VM(ingress) |
| ② 優先權 | 數字越大越先比對。Neutron 建的 SG 規則都是 1002 |
| ③ 比對條件 | outport == @pg_…:目的埠屬於這個 Port Group,也就是套用 c13-sg 的 VM(@ 表示引用一整組埠)。ip4.src == 0.0.0.0/0:來源不限,對應 SG 規則的 remote IP prefix。icmp4 / tcp.dst == 22:協定與目的埠 |
| ④ 動作 | allow-related:放行,而且記住這條連線,回應封包自動放行(stateful) |
| ACL | 對應的 c13-sg 規則 |
|---|---|
兩條 from-lport … ip4 / ip6 | 預設的 egress 規則:VM 往外全部放行 |
to-lport … icmp4 | ingress ICMP |
to-lport … tcp.dst == 22 | ingress TCP 22 |
沒有列在這裡的流量(例如進入 VM 的 TCP 80)會被丟棄:Neutron 把所有套用 SG 的埠另外放進一個叫 neutron_pg_drop 的 Port Group,上面有兩條優先權 1001 的 drop 規則(進、出各一條)。1002 的放行規則先比對,都沒符合才輪到 1001 的丟棄。F3 會用 ovn-trace 驗證。
B5. Subnet 沒有獨立物件
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl --columns=cidr,options find DHCP_Options cidr=192.168.13.0/24"預期輸出(節錄):
cidr : "192.168.13.0/24"
options : {classless_static_route="{169.254.169.254/32,192.168.13.2, 0.0.0.0/0,192.168.13.1}", dns_server="{127.0.0.53}", lease_time="43200", mtu="8492", router="192.168.13.1", server_id="192.168.13.1", ...}Subnet 在 OVN 裡沒有對應的物件(p.5),它的設定變成 OVN 內建 DHCP 服務要發給 VM 的參數:
options 的鍵 | 意思 |
|---|---|
router="192.168.13.1" | 發給 VM 的預設閘道,就是 Subnet 的 gateway_ip |
classless_static_route | 發給 VM 的靜態路由:169.254.169.254/32 經 192.168.13.2(metadata 走 localport),0.0.0.0/0 經 192.168.13.1 |
mtu="8492" | 發給 VM 的 MTU,就是 Network 的 MTU |
lease_time="43200" | DHCP 租期 12 小時 |
server_id | DHCP 回應的來源位址 |
Part C:第二層:SB DB,誰扛了什麼(p.12、13)
ovn-northd 把 NB 的邏輯物件編譯進 Southbound DB;每台節點的 ovn-controller 再向 SB DB 報到,認領自己負責的埠。這一層開始出現「節點」。
C1. 每台 Chassis 承載的 Port
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_sb_db ovn-sbctl show"預期輸出:
Chassis scp002-6164930-iaas
hostname: scp002-6164930-iaas
Encap geneve
ip: "10.127.0.18"
options: {csum="true"}
Port_Binding "a9edc7e7-6d03-47d0-a698-1166e7f3deb3"
Port_Binding "085ffce1-bdc4-4ebb-b59b-0fdc798666c1"
Port_Binding "16f96757-589b-4d89-a692-cbb69f53eb17"
Port_Binding "f7221753-088b-4710-9136-623f2c565c6f"
Port_Binding "a51cda76-2b69-456d-9e31-8871344dc6d3"
Chassis scp001-6161072-iaas
hostname: scp001-6161072-iaas
Encap geneve
ip: "10.127.0.7"
options: {csum="true"}
Port_Binding "1021e30d-24cf-4a41-8403-85852c7119e4"
Chassis ctl002-6161063-iaas
hostname: ctl002-6161063-iaas
Encap geneve
ip: "10.127.0.14"
options: {csum="true"}
Chassis ctl003-6161069-iaas
hostname: ctl003-6161069-iaas
Encap geneve
ip: "10.127.0.15"
options: {csum="true"}
Port_Binding cr-lrp-5c6c01e8-0f46-418c-91da-260f1e488cd6
Chassis ctl001-6161060-iaas
hostname: ctl001-6161060-iaas
Encap geneve
ip: "10.127.0.4"
options: {csum="true"}
Port_Binding cr-lrp-da5107b9-d39c-4621-b3f1-a9adf97f0b59怎麼讀這段輸出:它是「以節點為單位」的清單,每個 Chassis 區塊是一台節點,底下列出這台節點目前負責的埠。
| 輸出 | 意思 |
|---|---|
Chassis scp002-… | 一台加入 OVN 的節點。名稱是該節點 OVS 的 system-id,本環境剛好設成和主機名稱相同 |
Encap geneve / ip | 這台節點的 Tunnel 端點:別的節點要送封包給它時,用 Geneve 封裝後送到這個 IP |
options: {csum="true"} | Tunnel 封包會計算 checksum,可忽略 |
Port_Binding "<Port ID>" | 這台節點的 ovn-controller 認領了這個埠,代表那張 VM 網卡就插在這台的 br-int 上 |
Port_Binding cr-lrp-<ID> | chassis-redirect logical router port:Router 對外埠在這台節點「落地」,南北向流量都從這台出去 |
在輸出中找出:
| 找什麼 | 在哪一台 Chassis 底下 | 意義 |
|---|---|---|
vm-a 的 $PORT_A(1021e30d…) | scp001 | VM 在哪台,Port_Binding 就在哪台 |
vm-b 的 $PORT_B(f7221753…) | scp002 | vm-a 與 vm-b 不在同一台,兩者之間的封包要走 Geneve tunnel |
cr-lrp-5c6c01e8-…($GW_PORT) | ctl003(你看到的可能是另一台 ctl) | c13-router 的對外埠實際落點 |
cr-lrp-da5107b9-… | ctl001 | 另一個 Router(demo-router)的對外埠 |
| 沒有任何埠 | ctl002 | 沒有 VM,也不是任何 Router 目前的 Gateway。若環境中有其他 Router,它的 cr-lrp-… 也可能出現在這裡:每個 Router 各有一個,落點各自依優先權決定 |
- 5 台節點都有 Chassis 記錄:每台都跑 ovn-controller 並向 SB DB 報到。少了某一台,就是那台的 ovn-controller 有問題(p.12)
- scp002 上其他的 Port_Binding 屬於 demo1、Octavia amphora 等既有 VM
為什麼 cr-lrp 落在 ctl003? B2 的 gateway chassis 有三台候選,每台有一個優先權,數字最大且存活的那台承載 cr-lrp:
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl lrp-get-gateway-chassis lrp-$GW_PORT"預期輸出:
lrp-5c6c01e8-0f46-418c-91da-260f1e488cd6_ctl003-6161069-iaas 3
lrp-5c6c01e8-0f46-418c-91da-260f1e488cd6_ctl002-6161063-iaas 2
lrp-5c6c01e8-0f46-418c-91da-260f1e488cd6_ctl001-6161060-iaas 1ctl003 的優先權最高(3),所以 cr-lrp 在 ctl003。ctl003 故障時,cr-lrp 會自動移到 ctl002(2)。每個 Router 的優先權順序由 Neutron 分配,不一定相同。
C2. Gateway 節點與運算節點的差別
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_sb_db ovn-sbctl get Chassis ctl001-6161060-iaas other_config:ovn-bridge-mappings other_config:ovn-cms-options"
ssh 10.127.0.4 "sudo docker exec ovn_sb_db ovn-sbctl get Chassis scp001-6161072-iaas other_config:ovn-bridge-mappings other_config:ovn-cms-options"預期輸出(前兩行是 ctl001,後兩行是 scp001):
"physnet1:br-ex"
enable-chassis-as-gw
"physnet1:br-ex,physnet2:br-ex2"
""| ctl001(控制節點) | scp001(運算節點) | |
|---|---|---|
ovn-bridge-mappings | physnet1:br-ex | physnet1:br-ex,physnet2:br-ex2 |
ovn-cms-options | enable-chassis-as-gw | ""(空的) |
ovn-bridge-mappings的格式是<physical network 名稱>:<OVS bridge>,表示「這台節點的 physnet1 接在 br-ex 上」。Provider 網路(public1 是 physnet1)能不能在這台使用,就看這裡有沒有對應enable-chassis-as-gw:這台可以承載 cr-lrp。只有控制節點有,所以 C1 的 cr-lrp 都落在 ctl。這就是 p.6 說的「Gateway 合併至 Controller」"":沒有設定任何選項,所以運算節點不會成為 Gatewayphysnet2是 Octavia 管理網用的,只有運算節點需要- 投影片 p.13 寫的位置是
external_ids;本環境的 OVN 版本把它放在other_config,欄位位置會隨版本改變
Part D:第三層:本機 OVS(p.15)
前兩層看的是整個叢集的資料庫;OVS 只看本機,所以要到 vm-a 所在的 scp001 查。
D1. 這台機器上真正存在的介面
# [jmp001]
ssh 10.127.0.7 "sudo docker exec openvswitch_vswitchd ovs-vsctl show"預期輸出(節錄):
175b65a7-7146-4518-ba59-4e94025ba2b4
Bridge br-int
fail_mode: secure
datapath_type: system
Port ovn-scp002-0
Interface ovn-scp002-0
type: geneve
options: {csum="true", key=flow, local_ip="10.127.0.7", remote_ip="10.127.0.18"}
Port ovn-ctl001-0
Interface ovn-ctl001-0
type: geneve
options: {csum="true", key=flow, local_ip="10.127.0.7", remote_ip="10.127.0.4"}
bfd_status: {diagnostic="No Diagnostic", flap_count="1", forwarding="true", remote_diagnostic="No Diagnostic", remote_state=up, state=up}
...(ovn-ctl002-0、ovn-ctl003-0 與 ovn-ctl001-0 相同,只有 remote_ip 不同)
Port br-int
Interface br-int
type: internal
Port tap1021e30d-24
Interface tap1021e30d-24
Port tapcffbde07-10
Interface tapcffbde07-10
Bridge br-ex
fail_mode: standalone
Port br-ex
Interface br-ex
type: internal
Port ens4
Interface ens4
Bridge br-ex2
...怎麼讀這段輸出:Bridge 是一台虛擬交換器,底下每個 Port 是插在上面的一條線。
| 輸出 | 意思 |
|---|---|
Bridge br-int | 整合橋:所有 VM 網卡與 Tunnel 都插在這裡,OVN 的規則全部裝在它身上 |
fail_mode: secure | 沒有規則時一律丟棄,不會自己學習 MAC 轉送。所以 br-int 完全依賴 ovn-controller 下發的規則 |
datapath_type: system | 使用 Linux 核心的 OVS 模組轉送封包 |
tap1021e30d-24 | vm-a 的網卡,名稱是 tap + Neutron Port ID 前 11 碼($PORT_A) |
tapcffbde07-10 | c13-private 的 metadata localport,名稱帶 Network ID($NET) |
ovn-scp002-0,type: geneve | 通往 scp002 的 Tunnel;remote_ip 就是 C1 看到的 scp002 Encap IP |
ovn-ctl00X-0 的 bfd_status … state=up | 到 Gateway 節點的 Tunnel 會跑 BFD 心跳,確認對方活著;Gateway 掛掉時,cr-lrp 才能快速切到下一台。到 scp002 的 Tunnel 不需要,所以沒有 BFD |
Bridge br-ex + ens4 | 接 physnet1(外部網路)的 bridge,ens4 是實體網卡 |
- br-int 上沒有
patch-br-int-to-provnet-…:patch port(連 br-int 與 br-ex 的線)只有在本機需要直接接上 Provider 網路時才會建立。scp001 上沒有接在 public1 的 VM,Gateway 也不在這台,所以 vm-a 的對外流量是經ovn-ctl003-0這條 Tunnel 送到 Gateway 再出去
D2. 從 Port ID 找到 tap
# [jmp001]
ssh 10.127.0.7 "sudo docker exec openvswitch_vswitchd ovs-vsctl --columns=name,ofport,external_ids find Interface external_ids:iface-id=$PORT_A"預期輸出:
name : tap1021e30d-24
ofport : 25
external_ids : {attached-mac="fa:16:3e:73:0f:22", iface-id="1021e30d-24cf-4a41-8403-85852c7119e4", iface-status=active, ovn-installed="true", ovn-installed-ts="1793935759521", vm-uuid="a7750220-c2fb-44c6-892b-d11f214db292"}這個指令的意思是:在本機所有介面中,找出 external_ids:iface-id 等於 vm-a Port ID 的那一個。
| 欄位 | 意思 |
|---|---|
iface-id = Neutron Port ID | OVS 與 OVN 之間的橋樑。Nova 建立 tap 時寫入這個值,ovn-controller 看到後就知道「這個 tap 是邏輯埠 1021e30d…」,於是去 SB DB 認領,C1 的 Port_Binding 就是這樣出現的 |
attached-mac / vm-uuid | 這張網卡的 MAC,以及它屬於哪台 VM(Nova 的 server ID) |
ovn-installed="true" | ovn-controller 已經把這個埠需要的轉送規則裝好;是 false 或沒有這個欄位時,VM 網路通常不通 |
ofport = 25 | 這個介面在 br-int 上的 OpenFlow 埠號,ofproto/trace 的 in_port 要填這個數字(p.17) |
Part E:Provider 與 Self-Service(p.20–21)
# [jmp001]
openstack network show public1 -c provider:network_type -c provider:physical_network -c router:external
openstack network show c13-private -c provider:network_type -c provider:physical_network -c provider:segmentation_id
ssh 10.127.0.4 "sudo docker exec ovn_nb_db ovn-nbctl --columns=name,type,options find Logical_Switch_Port type=localnet"預期輸出:
+---------------------------+----------+
| Field | Value |
+---------------------------+----------+
| provider:network_type | flat |
| provider:physical_network | physnet1 |
| router:external | External |
+---------------------------+----------+
+---------------------------+--------+
| Field | Value |
+---------------------------+--------+
| provider:network_type | geneve |
| provider:physical_network | None |
| provider:segmentation_id | 1155 |
+---------------------------+--------+
name : provnet-406a2db9-da57-4935-b1fd-033478598467
type : localnet
options : {localnet_learn_fdb="false", mcast_flood="false", mcast_flood_reports="true", network_name=physnet1}
name : provnet-8cecc002-8242-47cc-a4ee-20a11f5a6605
type : localnet
options : {localnet_learn_fdb="false", mcast_flood="false", mcast_flood_reports="true", network_name=physnet2}怎麼讀這段輸出
| 輸出 | 意思 |
|---|---|
public1:flat、physnet1、External | Provider 網路:直接對映到實體網路 physnet1,沒有 VLAN tag;可當 Router 的對外閘道 |
c13-private:geneve、None、1155 | Self-Service 網路:不對映任何實體網路,用 Geneve 封裝,segmentation_id 1155 是它在 Tunnel 裡的編號(VNI) |
type: localnet | 一種特殊的 LSP,代表「通往實體網路的出口」 |
options: network_name=physnet1 | 這個出口接到 physnet1;每台節點再依 C2 的 ovn-bridge-mappings 找到 br-ex |
其他 options | 學習與廣播相關的細節設定,本課不需要 |
| public1 | c13-private | |
|---|---|---|
| 類型 | Provider(flat,physnet1) | Self-Service(geneve,VNI 1155) |
| OVN 的 localnet 埠 | 有:provnet-406a2db9-… | 沒有 |
| 封包怎麼離開 | localnet 埠 → 本機 br-ex → 實體網路 | Geneve tunnel;對外要經 Router(SNAT / FIP) |
在 OVN 裡,Provider 與 Self-Service 的差別就是 Logical Switch 上有沒有 localnet 埠(p.20)。全環境只有兩個 localnet:physnet1 是 public1,physnet2 是 Octavia 管理網。F 節會看到 vm-a 的封包最後就是從 provnet-406a2d… 這個埠離開。
Part F:第四層:ovn-trace,邏輯上應該怎麼走(p.16)
F0. 先看懂 ovn-trace 的輸入與輸出
ovn-trace 讀 SB DB 裡編譯好的 Logical Flow,模擬一個封包在邏輯網路裡會被怎麼處理。它不送真的封包,也不碰任何節點,所以可以放心執行。
輸入:描述一個封包
ovn-trace --minimal --ct new neutron-$NET 'inport=="…" && eth.src==… && ip4.src==… && eth.dst==… && ip4.dst==… && ip.ttl==64 && icmp4.type==8'
───────── ──────── ──────────── ─────────────────────────────────────────────────────────────────────────────────────────────
① 輸出量 ② 連線狀態 ③ 從哪個網路進入 ④ 封包長什麼樣子(用 && 串起來)| 參數 | 意思 |
|---|---|
① --minimal | 只列出封包「被改了什麼、從哪裡出去」,省略中間每一條規則。換成 --summary 可以看到經過的每個 Switch / Router,但輸出長很多 |
② --ct new | 假設這是一條新連線。SG 只在新連線時檢查,不加的話 ovn-trace 預設當成已建立的連線,看不出 SG 會不會擋 |
③ neutron-$NET | 封包從哪個邏輯網路開始走,就是 B1 的 Logical Switch 名稱 |
④ inport=="$PORT_A" | 從 vm-a 的網卡送出 |
④ eth.src / eth.dst | 來源 / 目的 MAC。VM 要送到外網時,目的 MAC 是閘道的 MAC($GW_MAC),不是 8.8.8.8 的 |
④ ip4.src / ip4.dst | 來源 / 目的 IP |
④ ip.ttl==64 | 封包的 TTL;經過 Router 會減 1 |
④ icmp4.type==8 | ICMP echo request,也就是 ping |
輸出:讀懂四種寫法就夠了
| 輸出長這樣 | 意思 |
|---|---|
第一行 # icmp,…,dl_src=…,nw_src=… | ovn-trace 把你的條件翻成 OpenFlow 的寫法再印一次,確認輸入無誤。dl_ = MAC、nw_ = IP、reg14 是 OVN 內部替輸入埠編的號碼,可以忽略 |
ct_next(…) { … } | 封包送進 conntrack(連線追蹤)查詢狀態,大括號裡是查完之後繼續做的事。巢狀越深,代表處理越後面 |
欄位 = 值;、ip.ttl--; | 封包被修改了,例如換來源 MAC、TTL 減 1 |
ct_snat(ip4.src=X) | 做 SNAT:來源 IP 被改成 X |
output("名稱") | 封包從這個埠送出去。名稱只印出前 6 碼:VM 埠是 Port ID 前 6 碼,provnet-406a2d 是 public1 的 localnet 埠 |
沒有任何 output | 封包被丟棄 |
F1. vm-a(有 FIP)連到 8.8.8.8
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_northd ovn-trace --minimal --ct new neutron-$NET 'inport==\"$PORT_A\" && eth.src==$MAC_A && ip4.src==$IP_A && eth.dst==$GW_MAC && ip4.dst==8.8.8.8 && ip.ttl==64 && icmp4.type==8'"預期輸出(左側加了行號方便對照,實際輸出沒有行號):
1 # icmp,reg14=0x3,vlan_tci=0x0000,dl_src=fa:16:3e:73:0f:22,dl_dst=fa:16:3e:61:02:68,nw_src=192.168.13.252,nw_dst=8.8.8.8,nw_tos=0,nw_ecn=0,nw_ttl=64,nw_frag=no,icmp_type=8,icmp_code=0
2 ct_next(ct_state=new|trk) {
3 ip.ttl--;
4 eth.src = fa:16:3e:f1:b7:52;
5 eth.dst = 00:00:00:00:00:00;
6 arp {
7 eth.dst = ff:ff:ff:ff:ff:ff;
8 arp.spa = 0xc0a864b9;
9 arp.tpa = 0xc0a86401;
10 arp.op = 1;
11 output("e474ff");
12 output("provnet-406a2d");
13 };
14 ct_dnat /* assuming no un-dnat entry, so no change */ {
15 ct_next(ct_state=est|trk /* default (use --ct to customize) */) {
16 ct_snat(ip4.src=192.168.100.156) {
17 output("provnet-406a2d");
18 };
19 };
20 };
21 };下圖把這段輸出畫成封包走過的路,右側是每一步對應的輸出行:
逐段說明
| 行 | 發生在哪裡 | 白話說明 |
|---|---|---|
| 1 | — | 你送進去的封包:從 vm-a(.252)送往閘道 MAC、目的 8.8.8.8 的 ping |
| 2 | c13-private(Switch) | 封包進入 conntrack,被當成新連線(new)。SG 的 egress 規則(B4 的 from-lport … allow-related)放行,所以繼續往下;若被擋,大括號裡會是空的 |
| 3 | c13-router(Router) | 經過一個 Router,TTL 從 64 減為 63 |
| 4 | c13-router | 查路由表(B2 的 0.0.0.0/0 → 192.168.100.1),決定從對外埠送出,來源 MAC 換成對外埠的 MAC fa:16:3e:f1:b7:52(B2 的 lrp-5c6c01e8-…) |
| 5–13 | c13-router → public1 | Router 還不知道下一跳 192.168.100.1 的 MAC,所以目的 MAC 先是全 0,並先送一個 ARP 詢問。arp.spa 是詢問者的 IP、arp.tpa 是要查的 IP,以 16 進位表示(見下方換算)。ARP 從 public1 的兩個埠送出:e474ff 是 public1 的 metadata localport,provnet-406a2d 是通往實體網路的 localnet 埠 |
| 14 | c13-router 的 Gateway(cr-lrp,在 ctl003) | 檢查這個封包是不是某條 DNAT 連線的回應;不是,所以不變 |
| 15 | 同上 | 第二次進入 conntrack。--ct 只指定了第一次的狀態,這次 ovn-trace 用預設的 est(已建立),註解 default (use --ct to customize) 就是在說這件事 |
| 16 | 同上 | 做 SNAT:來源 IP 從 192.168.13.252 改成 192.168.100.156,就是 vm-a 的 Floating IP,對應 B2 的 dnat_and_snat |
| 17 | public1(Switch) | 從 localnet 埠 provnet-406a2d 送出,也就是 E 節的 provnet-406a2db9-…,接著經 ctl003 的 br-ex 進入實體網路 |
16 進位 IP 換算:IP 的四段各是 0–255,正好是兩位 16 進位數。
| 16 進位 | 拆成四段 | 換成 10 進位 | 是誰 |
|---|---|---|---|
0xc0a864b9 | c0 a8 64 b9 | 192.168.100.185 | Router 的對外位址(詢問者) |
0xc0a86401 | c0 a8 64 01 | 192.168.100.1 | 實體路由器(要查 MAC 的對象) |
- 實際網路中,封包會等 ARP 回應拿到 MAC 後才送出;ovn-trace 只是把「先送 ARP」和「封包本身」兩條路都列出來
- 輸出中看不到「經過 Tunnel 送到 ctl003」:ovn-trace 只模擬邏輯層,不顯示封包實際在哪台節點處理。要知道 NAT 在哪台做,要回到 C1 看 cr-lrp 落在哪台
F2. vm-b(沒有 FIP)連到 8.8.8.8
指令和 F1 相同,只是來源換成 vm-b:
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_northd ovn-trace --minimal --ct new neutron-$NET 'inport==\"$PORT_B\" && eth.src==$MAC_B && ip4.src==$IP_B && eth.dst==$GW_MAC && ip4.dst==8.8.8.8 && ip.ttl==64 && icmp4.type==8'"預期輸出:
# icmp,reg14=0x4,vlan_tci=0x0000,dl_src=fa:16:3e:e8:53:d8,dl_dst=fa:16:3e:61:02:68,nw_src=192.168.13.82,nw_dst=8.8.8.8,nw_tos=0,nw_ecn=0,nw_ttl=64,nw_frag=no,icmp_type=8,icmp_code=0
ct_next(ct_state=new|trk) {
ip.ttl--;
eth.src = fa:16:3e:f1:b7:52;
eth.dst = 00:00:00:00:00:00;
arp {
eth.dst = ff:ff:ff:ff:ff:ff;
arp.spa = 0xc0a864b9;
arp.tpa = 0xc0a86401;
arp.op = 1;
output("e474ff");
output("provnet-406a2d");
};
ct_next(ct_state=est|trk /* default (use --ct to customize) */) {
ct_snat(ip4.src=192.168.100.185) {
output("provnet-406a2d");
};
};
};和 F1 逐項比較:
| F1:vm-a(有 FIP) | F2:vm-b(沒有 FIP) | |
|---|---|---|
第一行的 nw_src | 192.168.13.252 | 192.168.13.82 |
| TTL、換 MAC、ARP(行 3–13) | 相同 | 相同:同一個 Router、同一個出口 |
ct_dnat 那一行 | 有 | 沒有:vm-b 沒有 FIP,不需要檢查 DNAT |
ct_snat 改成 | 192.168.100.156(自己的 FIP) | 192.168.100.185(Router 的對外位址) |
| 對應 B2 的哪條 NAT | dnat_and_snat | snat |
同一個 Router、同一個 Subnet,有沒有 FIP 決定了出外網時的來源位址(C12 p.28)。外面的主機看到 .156 的封包,知道是 vm-a;看到 .185 的封包,分不出是子網裡哪一台。
F3. Security Group:vm-a → vm-b 的 ICMP 與 TCP 80
這次封包不出外網,而是從 vm-a 直接送到同一個子網的 vm-b,所以目的 MAC 直接填 vm-b 的 MAC。兩個指令只差最後的協定:
# [jmp001]
ssh 10.127.0.4 "sudo docker exec ovn_northd ovn-trace --minimal --ct new neutron-$NET 'inport==\"$PORT_A\" && eth.src==$MAC_A && ip4.src==$IP_A && eth.dst==$MAC_B && ip4.dst==$IP_B && ip.ttl==64 && icmp4.type==8'"
ssh 10.127.0.4 "sudo docker exec ovn_northd ovn-trace --minimal --ct new neutron-$NET 'inport==\"$PORT_A\" && eth.src==$MAC_A && ip4.src==$IP_A && eth.dst==$MAC_B && ip4.dst==$IP_B && ip.ttl==64 && tcp.dst==80'"預期輸出:
# icmp,reg14=0x3,vlan_tci=0x0000,dl_src=fa:16:3e:73:0f:22,dl_dst=fa:16:3e:e8:53:d8,nw_src=192.168.13.252,nw_dst=192.168.13.82,nw_tos=0,nw_ecn=0,nw_ttl=64,nw_frag=no,icmp_type=8,icmp_code=0
ct_next(ct_state=new|trk) {
ct_next(ct_state=est|trk /* default (use --ct to customize) */) {
output("f72217");
};
};
# tcp,reg14=0x3,vlan_tci=0x0000,dl_src=fa:16:3e:73:0f:22,dl_dst=fa:16:3e:e8:53:d8,nw_src=192.168.13.252,nw_dst=192.168.13.82,nw_tos=0,nw_ecn=0,nw_ttl=64,nw_frag=no,tp_src=0,tp_dst=80,tcp_flags=0
ct_next(ct_state=new|trk) {
ct_next(ct_state=est|trk /* default (use --ct to customize) */);
};逐段說明
| ICMP(ping) | TCP 80 | |
|---|---|---|
第一個 ct_next | 離開 vm-a:egress 規則放行 | 離開 vm-a:egress 規則放行 |
第二個 ct_next | 要進入 vm-b:符合 to-lport … icmp4 allow-related | 要進入 vm-b:沒有任何放行規則符合 |
| 結果 | output("f72217"):送到 vm-b 的埠($PORT_B 前 6 碼) | 大括號後直接是 ;,沒有 output:被丟棄 |
- 這兩個封包都只經過 c13-private 這個 Switch,沒有經過 Router:所以沒有
ip.ttl--,也沒有換 MAC、沒有 NAT - SG 是在要進入 vm-b 的那一刻擋下 TCP 80,因為 c13-sg 的 ingress 只開了 ICMP 與 TCP 22(B4)。把
tcp.dst==80改成tcp.dst==22再試一次,就會看到output("f72217") - 實際上 vm-a 在 scp001、vm-b 在 scp002,封包會走 D1 看到的
ovn-scp002-0tunnel;ovn-trace 只看邏輯層,不會顯示 tunnel
延伸:ofproto/trace(p.17):ovn-trace 走得通、實際卻不通時,到 VM 所在節點用
ovs-appctl ofproto/trace br-int in_port=<D2 的 ofport>,…追蹤本機 br-int 上真正的 OpenFlow 規則。輸出很長,本課不實作。
小結:同一組資源在四層中的樣子
| Neutron 資源 | B. NB(ovn-nbctl) | C. SB(ovn-sbctl) | D. OVS(ovs-vsctl) | F. ovn-trace |
|---|---|---|---|---|
| Network c13-private | switch … (neutron-$NET) | — | — | 第一個參數 neutron-$NET |
| vm-a Port | port 1021e30d-… | Port_Binding 在 scp001 | tap1021e30d-24,iface-id = Port ID | inport |
| Router c13-router | router … (neutron-$ROUTER) | — | — | eth.dst = $GW_MAC 進入 Router,ip.ttl-- |
| Router Gateway Port | lrp-5c6c01e8-…、gateway chassis | cr-lrp-5c6c01e8-… 在 ctl003 | ctl 上的 br-ex | eth.src = fa:16:3e:f1:b7:52 |
| vm-a 的 FIP | nat dnat_and_snat | — | — | ct_snat(…156) |
| Router SNAT | nat snat | — | — | ct_snat(…185)(vm-b) |
| c13-sg | Port Group + ACL | — | — | ICMP output、TCP 80 無 output |