文章目录
1 开发环境介绍
1.系统环境:Ubuntu
2.软件环境:
jdk 1.8
maven 3.3.9
OpenDaylight
Mininet
Eclipse/IntelliJ IDEA
2 构建工程
2.1 生成项目骨架
根据OpenDaylight官方文档提供的Maven命令生成项目骨架
mvn archetype:generate -DarchetypeGroupId=org.opendaylight.controller -DarchetypeArtifactId=opendaylight-startup-archetype -DarchetypeRepository=http://nexus.opendaylight.org/content/repositories/opendaylight.release/ -DarchetypeCatalog=remote -DarchetypeVersion=1.5.1
构建成功后会在当前目录生成项目文件夹,进入到文件夹下即可看到工程目录结构
2.2 编译工程
对构建好的工程进行编译
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
2.3 启动控制器
启动控制器,具体启动如下图
2.4 导入项目到开发IDE中
3 实例开发
3.1 RPC和YANG模型
3.1.1 什么是RPC
RPC 全称 Remote Procedure Call——远程过程调用。在学校学编程,我们写一个函数都是在本地调用就行了。但是在互联网公司,服务都是部署在不同服务器上的分布式系统,如何调用呢?
RPC技术简单说就是为了解决远程调用服务的一种技术,使得调用者像调用本地服务一样方便透明。
OpenDaylight采用RPC这种通信方式主要是基于以下几点:
- 可以实现跨语言跨平台调用,不同业务系统都可以进行调用OpenDaylight对外提供的服务,使得OpenDaylight服务更具通用性;
- 每个调用方只需要提出需求,不需要了解OpenDaylight具体的代码逻辑实现;
- RPC易于拓展,易于复用。
3.1.2 什么是YANG模型
早在2003年,IETF成立了一个NETCONF工作组,提出一种基于XML的网络配置管理协议,也就是NETCONF(Network Configuration Protocol),因为该协议的配置功能非常强大,所以广泛采用来配置网络。
NETCONF协议分为传输层、RPC层、操作层和内容层。其中,内容层是唯一没有标准化的层,于是一种新的建模语言YANG产生了,它的目标是对NETCONF数据模型、操作进行建模,覆盖NETCONF协议的操作层和内容层。
YANG模型的产生主要是对以下三类数据进行建模:
- 远程过程调用(rpc):对可在网元上触发的远程程序调用进行建模;
- 资源(data):对要操控的资源进行建模,也就是对网元设备的的configure或者operational数据进行建模;
- 通知(notification):对网元发出的事件通知消息进行建模。
OpenDaylight控制器目前主要采用MD-SAL架构,这里的M指的就是YANG 模型。在开发过程中,一般先根据需求先设计YANG模型来确定准备提供哪些数据和服务,然后使用Maven进行编译时,通过YANG Tools插件将YANG文件的转译为相应的java类、接口。开发者通过实现自动生成的Java代码来实现具体的API和服务内容。
由于YANG文件通俗易懂,我们完全可以仅仅通过YANG文件就可以大概了解到系统提供的数据和服务内容,十分的方便。同时,OpenDaylight访问Data Store的API完全基于Yang模型,这部分代码Yang Tools插件自动生成,这样开发人员只需关注如何进行提供服务,大大提高了开发效率和降低了OpenDaylight控制器的开发难度。
3.1 YANG模型中定义RPC
-
编辑api/src/main/yang目录下的topology.yang,进行yang文件编写,完整的topology.yang文件如下:
module topology { yang-version 1; namespace "urn:opendaylight:params:xml:ns:yang:topology"; prefix "topology"; import ietf-inet-types { prefix inet; revision-date 2013-07-15; } description "get links and ports information"; revision "2018-03-07" { description "Initial revision of topology model"; } grouping output-link{ leaf link-id{ type inet:uri; description "link id"; } leaf src-device{ type inet:uri; description "source device"; } leaf src-port{ type int32; description "source port"; } leaf dst-device{ type inet:uri; description "destination device"; } leaf dst-port{ type int32; description "destination port"; } } grouping output-port{ leaf device-id{ type inet:uri; description "link id"; } leaf port-number{ type string; description "port number"; } leaf port-name{ type string; description "port name"; } leaf hardware-address{ type string; description "hardware address"; } leaf current-speed{ type int64; description "current speed"; } leaf maximum-speed{ type int64; description "maximum speed"; } leaf link-down{ type boolean; description "link down"; } } rpc list-links-info{ output { list links-info{ uses output-link; description "link info"; } } } rpc list-ports-info{ output{ list ports-info{ uses output-port; description "port info"; } } } }
解析:
①module:在实际项目中,一个工程中YANG模型很多,所以可以通过module来划分不同模块的YANG。各个module描述各自的数据模型,不同module之间可以互相引用,十分灵活。②namespace和prefix :每个module有一个独立的namespace以避免命名冲突,同时还有一个简称prefix。
③import:通过import导入外部模块,可以在本模块中使用导入模块中定义的数据结构或数据类型。导入时会给外部模块起一个prefix,引用导入模块中的内容时以prefix开头即可。
④description:描述YANG文件的功能或者各个节点的功能。
⑤revision:版本信息。
⑥grouping:一种可重用的schema nodes的集合。该集合可能在定义处的module中被使用,也可能在包含它的modules中使用,还可能在导入它的modules中被使用。grouping不会生成任何节点,可以看成一种临时树干。
⑦leaf:一种数据节点,在data tree中最多只能有一个实例存在。一个leaf节点只能有一个值,并且不能有子节点。leaf主要用来定义属性值。
⑧type:指定leaf的数据类型,该类型可以是YANG RFC规定的基本类型,也可以是自定义的派生类型。在YANG文件中,int32为基本类型,inet:uri为派生类型,派生类型的定义由typedef定义。基本的原生数据类型有uint8、uint16、string、bits、binary、boolean等等。
⑨rpc:远程过程调用。它可能包含input和output子节点,分别是该rpc所需要的输入和输出数据结构。若没有则表明该操作不需要输入数据或者没有输出数据。
⑩uses:实例化在grouping声明中定义的schema nodes。
更为详细的YANG语法知识可以参考rfc 6020(https://tools.ietf.org/html/rfc6020)
需要注意的是,由于在YANG文件中引入了ietf-inet-types,需要在api目录下的pom.xml中添加相关依赖:
<dependencies> <dependency> <groupId>org.opendaylight.mdsal.model</groupId> <artifactId>ietf-inet-types-2013-07-15</artifactId> <version>1.2.2-Carbon</version> </dependency> </dependencies>
Tip:dependencies标签如果存在则直接添加dependency这一层标签即可。
-
定义好YANG文件,需要重新编译工程,编译时无需整体编译,只需进入到工程主目录下的api目录下进行编译即可,具体命令如下:
cd api/ mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
-
编译时,maven会通过Yang Tools插件生成基本的Java文件,生成的Java文件如下图所示:
3.2 RPC实现
在OpenDaylight中默认采用是DataStore这种数据库。DataStore采用树型结构进行数据存储,它是一种高效的In-Memory(内存)数据库。
DataStore中的数据存储分两种形式:config和operational。config持有由应用所写的数据,一般是设备的配置信息或者是需要持久化的数据,而operational反映了设备运行时的数据,表明设备的运行状态,从设备读取数据,如果没有错误即可以看到设备的当前实际信息。
config和operational相同之处在于两者的数据都以树型节点的形式存储在内存中;不同点是config数据生命周期比较长,控制器断电重启后数据会被持久化到硬盘中,持久化主要采用快照(snapshot)和增量日志(journal)这两种方式,而operational数据生命周期比较短,控制器断电重启后数据失效。
本案例中的全网拓扑信息以及获取网卡信息都是从OpenDaylight的operational数据库中读取出来的状态数据。
由于DataStore的内容比较多,我们小组将会在后面的系列文章中,对DataStore的知识进行深入探讨。
3.2.1 编写服务提供端service代码
impl/src/main/java/org/opendaylight/topology/impl/TopologyProvider.java文件进行代码实现,由于代码比较少,这里贴出TopologyProvider.java的所有代码(不包括package和import):
public class TopologyProvider implements TopologyService {
private static final Logger LOG = LoggerFactory.getLogger(TopologyProvider.class);
private final DataBroker dataBroker;
private static final String FLOWID = "flow:1";
public TopologyProvider(final DataBroker dataBroker) {
this.dataBroker = dataBroker;
}
public void init() {
LOG.info("TopologyProvider Session Initiated");
}
public void close() {
LOG.info("TopologyProvider Closed");
}
@Override
public Future<RpcResult<ListLinksInfoOutput>> listLinksInfo() {
final SettableFuture<RpcResult<ListLinksInfoOutput>> futureResult = SettableFuture.create();
ListLinksInfoOutputBuilder outputBuilder = new ListLinksInfoOutputBuilder();
final InstanceIdentifier.InstanceIdentifierBuilder<Topology> topologyId = InstanceIdentifier.builder(NetworkTopology.class).
child(Topology.class, new TopologyKey(new TopologyId(new Uri(FLOWID))));
InstanceIdentifier<Topology> topologyIId = topologyId.build();
Topology topology = read(LogicalDatastoreType.OPERATIONAL, topologyIId);
if (topology == null || topology.getLink() == null || topology.getLink().size() < 1) {
futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
return futureResult;
}
List<LinksInfo> linkInfos = new ArrayList<>();
topology.getLink().forEach(temp -> {
LinksInfoBuilder lib = new LinksInfoBuilder();
lib.setLinkId(temp.getLinkId())
.setSrcDevice(temp.getSource().getSourceNode())
.setDstPort(getPort(temp.getSource().getSourceTp().getValue()))
.setDstDevice(temp.getDestination().getDestNode())
.setDstPort(getPort(temp.getDestination().getDestTp().getValue()));
linkInfos.add(lib.build());
});
outputBuilder.setLinksInfo(linkInfos);
futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
return futureResult;
}
@Override
public Future<RpcResult<ListPortsInfoOutput>> listPortsInfo() {
final SettableFuture<RpcResult<ListPortsInfoOutput>> futureResult = SettableFuture.create();
ListPortsInfoOutputBuilder listPortsInfoOutputBuilder = new ListPortsInfoOutputBuilder();
Nodes nodes = queryAllNode(LogicalDatastoreType.OPERATIONAL);
if (nodes == null || nodes.getNode() == null || nodes.getNode().size() < 1) {
futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
return futureResult;
}
List<PortsInfo> portsInfos = new ArrayList<>();
nodes.getNode().forEach(tempNode -> {
List<NodeConnector> nodeConnectors = filterNodeConnectors(tempNode);
if (nodeConnectors == null || nodeConnectors.size() < 1) {
return;
}
nodeConnectors.forEach(tempPort -> {
PortsInfoBuilder pi = new PortsInfoBuilder();
FlowCapableNodeConnector augmentation = tempPort.getAugmentation(FlowCapableNodeConnector.class);
pi.setDeviceId(tempNode.getId())
.setPortNumber(new String(augmentation.getPortNumber().getValue()))
.setPortName(augmentation.getName())
.setHardwareAddress(augmentation.getHardwareAddress().getValue())
.setLinkDown(augmentation.getState().isLinkDown())
.setMaximumSpeed(augmentation.getMaximumSpeed())
.setCurrentSpeed(augmentation.getCurrentSpeed());
portsInfos.add(pi.build());
});
});
listPortsInfoOutputBuilder.setPortsInfo(portsInfos);
futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
return futureResult;
}
private <D extends DataObject> D read(final LogicalDatastoreType store, final InstanceIdentifier<D> path) {
D result = null;
final ReadOnlyTransaction transaction = dataBroker.newReadOnlyTransaction();
Optional<D> optionalDataObject;
final CheckedFuture<Optional<D>, ReadFailedException> future = transaction.read(store, path);
try {
optionalDataObject = future.checkedGet();
if (optionalDataObject.isPresent()) {
result = optionalDataObject.get();
} else {
LOG.debug("{}: Failed to read {}", Thread.currentThread().getStackTrace()[1], path);
}
} catch (final ReadFailedException e) {
LOG.warn("Failed to read {} ", path, e);
}
transaction.close();
return result;
}
private int getPort(String tp) {
return Integer.parseInt(tp.split(":")[2]);
}
private Nodes queryAllNode(LogicalDatastoreType configuration) {
final InstanceIdentifier<Nodes> identifierNodes = InstanceIdentifier.create(Nodes.class);
return read(configuration, identifierNodes);
}
private List<NodeConnector> filterNodeConnectors(Node node) {
final List<NodeConnector> connectors = Lists.newArrayList();
final List<NodeConnector> list = node.getNodeConnector();
if (list != null && list.size() > 0) {
for (final NodeConnector nodeConnector : list) {
if (!nodeConnector.getId().getValue().endsWith("LOCAL")) {
connectors.add(nodeConnector);
}
}
}
return connectors;
}
}
获取网络拓扑信息(listLinks方法)的大致流程如下:
①从network-topology的operational数据库中读取“flow:1”节点的所有内容。
②过滤“flow:1”下的节点内容,从而获取所需的link信息。
③将获取到的link信息封装成Future<RpcResult>进行返回给消费端。
获取网卡信息(listPortsInfo方法)的大致流程如下:
①从opendaylight-inventory中获取所有的Node节点,Node表示连接在控制器上的SDN交换机设备。
②获取每个Node节点的所有NodeConnector,然后获取需要的NodeConnector上的参数信息,NodeConnector相当于交换机的端口。
③将获取到信息封装成Future<RpcResult>进行返回给消费端。
3.2.2 完善相关依赖
我们在代码中使用了ietf-topology、model-inventory、model-flow-service,需要在impl目录的pom.xml文件中添加如下依赖。
<dependency>
<groupId>org.opendaylight.mdsal.model</groupId>
<artifactId>ietf-topology</artifactId>
<version>2013.10.21.10.2-Carbon</version>
</dependency>
<dependency>
<groupId>org.opendaylight.controller.model</groupId>
<artifactId>model-inventory</artifactId>
<version>1.5.2-Carbon</version>
</dependency>
<dependency>
<groupId>org.opendaylight.openflowplugin.model</groupId>
<artifactId>model-flow-service</artifactId>
<version>0.4.2-Carbon</version>
</dependency>
与之相对应的是需要在features目录下的src/main/features/features.xml文件添加引用仓库:
<repository>
mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features
</repository>
同时也需要在features.xml文件中的name为odl-topology-api的feature标签中添加如下feature:odl-openflowplugin-flow-services,该句添加位置如下:
<feature name='odl-topology-api' version='${project.version}' description='OpenDaylight :: topology:: api'>
<feature version='${mdsal.model.version}'>odl-mdsal-models</feature>
<feature version="${openflowplugin.version}">odl-openflowplugin-flow-services</feature>
<bundle>mvn:org.opendaylight.topology/topology-api/{{VERSION}}</bundle>
</feature>
其中openflowplugin.version在features目录下pom.xml文件中定义,需要在其中的properties标签内添加:
<openflowplugin.version>0.4.2-Carbon</openflowplugin.version>
4 RPC注册
编写完RPC实现的代码后,需要对RPC进行注册到OpenDaylight控制器以供外部调用,注册RPC方式有多种,后面的系列文章中会对RPC注册问题作详细叙述,本案例中采用的是Blueprint方式来注册RPC。
Blueprint是针对OSGi的依赖注入解决方案,用法非常类似Spring。当使用服务的时候,Blueprint会马上创建并注入一个代理(Proxy)。对这些服务进行调用时,如果服务在当前不可用的话,将会产生阻塞,直至能够获取到服务或超时。
其实就是我们把一些原本需要写代码去实现的内容用xml文件的方式简化了,这个xml文件固定放在bundle的src\main\resources\org\opendaylight\blueprint目录下,否则获取不到文件内容,至于文件名可以随意。可以想象到有专门去解析这个xml文件的代码,这个代码的位置就在controller\opendaylight\blueprint目录下,有兴趣的读者可以去看下源码。
具体操作过程:在impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml中添加:<odl:rpc-implementation ref=“provider”/>,其中provider是实例化类的id,id名称可以改变但必须两者保持一致。
impl-blueprint.xml文件内容如下:
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
odl:use-default-for-reference-types="true">
<reference id="dataBroker"
interface="org.opendaylight.controller.md.sal.binding.api.DataBroker"
odl:type="default" />
<bean id="provider"
class="org.opendaylight.topology.impl.TopologyProvider"
init-method="init" destroy-method="close">
<argument ref="dataBroker" />
</bean>
<odl:rpc-implementation ref="provider"/>
</blueprint>
5 RPC测试
RPC测试需要用到OpenDaylight WEB控制台和l2switch相关组件。因此在编译项目之前,需要在features目录下的src/main/features/features.xml文件添加引用仓库:
<repository>
mvn:org.opendaylight.l2switch/features-l2switch/${l2switch.version}/xml/features
</repository>
5.1 启动服务
编译项目
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
./karaf/target/assembly/bin/karaf
启动完成后,可以在karaf控制台命令查看开发的bundle是否安装成功,如果显示Active表示安装成功,具体命令为:
5.2 调用RPC
使用OpenDaylight WEB控制台的Yangman进行调用RPC,登录到WEB控制台后,依次鼠标点击Yangman—>topology rev.2018.03.07—>operations
补充:ODL中RPC接口的restconf化原理
参考:https://blog.csdn.net/sunquan291/article/details/82494264