---
tags: 技術文章, SDN, P4, PI, P4Runtime, ONOS
GA: UA-79596126-4
title: ONOS 與 P4 switch
---
###### 作者: 大叔
###### 撰寫日期: 2020/04/08
{%hackmd BJrTq20hE %}
# 目錄
[TOC]
# ONOS 與 P4
![](https://i.imgur.com/HPk0acL.png)
如果要在 ONOS 上使用 P4 主要是用以下這些步驟進行開發
1. 撰寫 P4 程式
2. 編譯後得到 P4Info 文件
3. 撰寫與編譯 Pipeconf 應用程式,此時會將 P4Info、BMv2 JSON、Tofino Binary 等相關文件打包成 oar 檔
4. 撰寫控制應用程式,可以是 Pipeline-agnostic 或是 Pipeline-aware
而其代表的意義如下
* Pipeline-agnostic application:
這類型的應用程式並不會知道即將面對的裝置所用的 Pipeline 長怎麼樣,僅會專心去做要做的事情,舉例來說 ProxyArp 或是 LLDP provider 等等。
* Pipeline-aware application:
這類型的則是針對特定 Pipeline 所設計的,會直接產生出特定的 Flow 以及 Group,並直接安裝到特定的 Table 中。
# ONOS 與 PI
講到 P4 的控制層面一定會想到 PI(protocol independent) 架構
雖然也是有人使用 OpenFlow 來當作控制協議
但其在彈性上並不是很好
故目前大多數人會傾向使用 P4Runtime
![](https://i.imgur.com/GrBL5wX.png)
上圖是 PI 在 ONOS 架構中的設計
層級可分為三層分別是 Protocol、Driver 與 Core
在 Protocol 層有 P4Runtime,其是透過 gRPC 的方式進行溝通
往上一層是 Driver 層,依照所使用的設備選擇驅動
目前支援 Barefoot Tofino, Mellanox Spectrum 與 BMv2 ... 等
在往上一層是 Core 層,也就是 PI 架構所在的一層
其包含三大模組 PI module、Flow Rule Translation 以及 Pipeconf
若 ONOS 要控制底層 P4 交換器則需撰寫 Pipeconf 應用
# 透過 ONOS 控制 bmv2 switch
![](https://i.imgur.com/VAvgwfy.png)
當寫好一個 P4 檔如果需要控制器去控制的話
ONOS 控制器是一個很好的選擇 其提供 Pipeconf 的方式
讓 ONOS 知道底層 P4 交換器有哪些 pipeline 與 table
ONOS 目前有提供一些 default pipeconf 給使用者選擇
也就是 basic 與 fabric
一個完整的 pipeconf 包含以下元素
* pipeline config ( bmv2 json, Tofino binary… )
* p4info
* PipeconfLoader (進入點)
* Interpreter ( 可有可無 )
* Pipeliner ( 可有可無 )
這邊主要會以 PipeconfLoader、Interpreter、Pipeliner 的撰寫來講解
### 撰寫 PipeconfLoader
當 ONOS 啟動 PipeconfLoader 時,會透過 PiPipeconfService 去註冊 Pipeconf
此程式為 Pipeconf 的核心以及程式的進入點
舉例來說如果希望 Pipeconf 擁有 Interpreter 以及 Pipeliner 兩種 Driver Behavior
以及使用 bmv2 json + p4info,可以這樣寫
```java
PiPipeconfId id = new PiPipeconfId("my-test-pipeconf");
URL bmv2ConfigPath = TestPipeconfLoader.class.getResource("/test-pipeline.json");
URL p4infoPath = TestPipeconfLoader.class.getResource("/test-pipeline.p4info");
pipeconf = DefaultPiPipeconf.builder()
.withId(id)
.addBehaviour(Pipeliner.class, TestPipeliner.class)
.addBehaviour(PiPipelineInterpreter.class, TestInterpreter.class)
.addExtension(PiPipeconf.ExtensionType.BMV2_JSON, bmv2ConfigPath)
.addExtension(PiPipeconf.ExtensionType.P4_INFO_TEXT, p4infoPath)
.build();
piPipeconfService.register(pipeconf);
```
上述的程式可以放在 activate function 中
ONOS 安裝這個 App(oar)時就會將 Pipeconf 載入
### 撰寫 Interpreter
Interpreter 主要是處理 ONOS API 轉換至 PI API 的工作
* mapCriterionType: 將 ONOS Criterion 轉換成 PI match 欄位
* mapPiMatchFieldId: 將 PI match 欄位轉換成普通的 Criterion
```java
@Override
public Optional<PiMatchFieldId> mapCriterionType(Criterion.Type type) {
if (CRITERION_MAP.containsKey(type)) {
return Optional.of(PiMatchFieldId.of(CRITERION_MAP.get(type)));
} else {
return Optional.empty();
}
}
```
* mapFlowRuleTableId: 將數字 Id 轉換成像是 P4 一樣使用字串的 Id
* mapPiTableId: 將字串 Id 轉換回數字 Id
```java
@Override
public Optional<PiTableId> mapFlowRuleTableId(int flowRuleTableId) {
return Optional.empty();
}
```
* mapTreatment: 將 ONOS 的 TrafficTreatment 加上 TableId 轉換成 PiAction,這主要是要解決多個 Action 對到單一個 Action 的問題
```java
@Override
public PiAction mapTreatment(TrafficTreatment treatment, PiTableId piTableId)
throws PiInterpreterException {
throw new PiInterpreterException("Treatment mapping not supported");
}
```
* mapOutboundPacket: 將 ONOS PacketOut 轉換成 PI 格式,即 metadata + paylad
```java
@Override
public Collection<PiPacketOperation> mapOutboundPacket(OutboundPacket packet)
throws PiInterpreterException {
TrafficTreatment treatment = packet.treatment();
// Packet-out in main.p4 supports only setting the output port,
// i.e. we only understand OUTPUT instructions.
List<OutputInstruction> outInstructions = treatment
.allInstructions()
.stream()
.filter(i -> i.type().equals(OUTPUT))
.map(i -> (OutputInstruction) i)
.collect(toList());
if (treatment.allInstructions().size() != outInstructions.size()) {
// There are other instructions that are not of type OUTPUT.
throw new PiInterpreterException("Treatment not supported: " + treatment);
}
ImmutableList.Builder<PiPacketOperation> builder = ImmutableList.builder();
for (OutputInstruction outInst : outInstructions) {
if (outInst.port().isLogical() && !outInst.port().equals(FLOOD)) {
throw new PiInterpreterException(format(
"Packet-out on logical port '%s' not supported",
outInst.port()));
} else if (outInst.port().equals(FLOOD)) {
// To emulate flooding, we create a packet-out operation for
// each switch port.
final DeviceService deviceService = handler().get(DeviceService.class);
for (Port port : deviceService.getPorts(packet.sendThrough())) {
builder.add(buildPacketOut(packet.data(), port.number().toLong()));
}
} else {
// Create only one packet-out for the given OUTPUT instruction.
builder.add(buildPacketOut(packet.data(), outInst.port().toLong()));
}
}
return builder.build();
}
```
* mapInboundPacket: 將 ONOS PacketIn 轉換成 PI 格式
```java
@Override
public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId)
throws PiInterpreterException {
// Find the ingress_port metadata.
final String inportMetadataName = "ingress_port";
Optional<PiPacketMetadata> inportMetadata = packetIn.metadatas()
.stream()
.filter(meta -> meta.id().id().equals(inportMetadataName))
.findFirst();
if (!inportMetadata.isPresent()) {
throw new PiInterpreterException(format(
"Missing metadata '%s' in packet-in received from '%s': %s",
inportMetadataName, deviceId, packetIn));
}
// Build ONOS InboundPacket instance with the given ingress port.
// 1. Parse packet-in object into Ethernet packet instance.
final byte[] payloadBytes = packetIn.data().asArray();
final ByteBuffer rawData = ByteBuffer.wrap(payloadBytes);
final Ethernet ethPkt;
try {
ethPkt = Ethernet.deserializer().deserialize(
payloadBytes, 0, packetIn.data().size());
} catch (DeserializationException dex) {
throw new PiInterpreterException(dex.getMessage());
}
// 2. Get ingress port
final ImmutableByteSequence portBytes = inportMetadata.get().value();
final short portNum = portBytes.asReadOnlyBuffer().getShort();
final ConnectPoint receivedFrom = new ConnectPoint(
deviceId, PortNumber.portNumber(portNum));
return new DefaultInboundPacket(receivedFrom, ethPkt, rawData);
}
```
### 撰寫 Pipeliner
Pipeliner 是專門將 FlowObjective 轉換成 Flow + Group 的一個組件
* FilteringObjective:用來表示允許或是擋掉封包進入 Pipeliner 的規則
```java
@Override
public void filter(FilteringObjective obj) {
obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED));
}
```
* ForwardingObjective:用來描述封包在 Pipeliner 中需要如何去處理
```java
@Override
public void forward(ForwardingObjective obj) {
if (obj.treatment() == null) {
obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED));
}
// Whether this objective specifies an OUTPUT:CONTROLLER instruction.
final boolean hasCloneToCpuAction = obj.treatment()
.allInstructions().stream()
.filter(i -> i.type().equals(OUTPUT))
.map(i -> (Instructions.OutputInstruction) i)
.anyMatch(i -> i.port().equals(PortNumber.CONTROLLER));
if (!hasCloneToCpuAction) {
// We support only objectives for clone to CPU behaviours (e.g. for
// host and link discovery)
obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED));
}
// Create an equivalent FlowRule with same selector and clone_to_cpu action.
final PiAction cloneToCpuAction = PiAction.builder()
.withId(PiActionId.of(CLONE_TO_CPU))
.build();
final FlowRule.Builder ruleBuilder = DefaultFlowRule.builder()
.forTable(PiTableId.of(ACL_TABLE))
.forDevice(deviceId)
.withSelector(obj.selector())
.fromApp(obj.appId())
.withPriority(obj.priority())
.withTreatment(DefaultTrafficTreatment.builder()
.piTableAction(cloneToCpuAction).build());
if (obj.permanent()) {
ruleBuilder.makePermanent();
} else {
ruleBuilder.makeTemporary(obj.timeout());
}
final GroupDescription cloneGroup = Utils.buildCloneGroup(
obj.appId(),
deviceId,
CPU_CLONE_SESSION_ID,
// Ports where to clone the packet.
// Just controller in this case.
Collections.singleton(PortNumber.CONTROLLER));
switch (obj.op()) {
case ADD:
flowRuleService.applyFlowRules(ruleBuilder.build());
groupService.addGroup(cloneGroup);
break;
case REMOVE:
flowRuleService.removeFlowRules(ruleBuilder.build());
groupService.removeGroup(deviceId, cloneGroup.appCookie(), obj.appId());
break;
default:
log.warn("Unknown operation {}", obj.op());
}
obj.context().ifPresent(c -> c.onSuccess(obj));
}
```
* NextObjective:用來描述 Egress table 裡面需要放置什麼樣的東西
```java
@Override
public void next(NextObjective obj) {
obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED));
}
@Override
public List<String> getNextMappings(NextGroup nextGroup) {
// We do not use nextObjectives or groups.
return Collections.emptyList();
}
```
# 參考文獻
* [P4 台灣社群](https://p4tw.org/)
* [ONOS+P4 Tutorial for Beginners](https://wiki.onosproject.org/pages/viewpage.action?pageId=16122675)