Flask模拟NRF
Posted 吴大卫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flask模拟NRF相关的知识,希望对你有一定的参考价值。
1.说明
1)本文主要介绍:
如何使用Python的Flask模块来模拟NRF。
如何使用浏览器和Python模拟发起5G网元的注册、更新、注销、发现等流程。
2)用到的素材包括:
Ubuntu虚机(Server):版本任意,用来模拟NRF,所有Flask脚本都跑在上面。
Windows电脑:要有浏览器和Python3,用来模拟Client访问NRF。
3)连接拓扑如图:
4)声明:代码中出现的IMSI,FQDN等信息仅为案例参考,请勿套用实际工作。
5)本文所有材料已打包:
https://share.weiyun.com/vkPFVU8q
2.Server安装Flask模块
1)首先安装Flask:
pip install flask
3)创建Flask的工作目录:
mkdir -p mypro/templates # Flask默认用来存网页模板
mkdir -p mypro/static # Flask默认用来存静态数据
mkdir -p mypro/5gc # 用来存模拟的5GC网元数据
cd mypro # 跳转到Flask的工作目录
3.第一个Flask脚本:Hello world!
1)先体验一下Flask,在Ubuntu虚机中写脚本:vi app.py
# _*_ coding: utf-8 _*_
# 设置utf-8编码支持中文,否则中文注释运行脚本可能会报错
# 导入flask模块的Flask包
from flask import Flask
# 实例化flask对象
app = Flask(__name__)
# flask内置装饰器,能让对URL'/'的访问交给hello_world函数处理
def hello_world():
# 返回字符串Hello World
return 'Hello, World!'
2)运行Flask:
# 写环境变量,方便测试
cat << EOF >> ~/.bashrc
# 默认运行的脚本名称
export FLASK_APP=app.py
# 打开开发者开关,这样修改app后不用重启脚本即可自动重载配置
export FLASK_ENV=development
# 打开调试功能,这样网页可以直线显示报错,加快定位
export FLASK_DEBUG=1
EOF
source ~/.bashrc
# 正式运行,这里是用nohup放到后台运行
# 默认监听地址127.0.0.1,可用--host自定义
# 默认监听端口5000,可用--port自定义
nohup flask run --host 192.168.70.138 > /var/log/flask.log 2>&1 &
3)电脑浏览器访问虚机:
http://192.168.70.138:5000
运行成功。
4.Flask的工作过程
1)用户在浏览器输入URL,访问某个资源。
2)Flask接收用户请求并分析请求的URL。
3)为这个URL找到对应的处理函数。
4)执行函数并生成响应,返回给浏览器。
5)浏览器接收并解析响应,将信息显示在页面中。
这是标准的CS(Client/Server)模式。
5.NRF的工作过程
1)5G网络中,NRF主要提两种服务:
NF管理:注册、更新、注销、状态订阅,状态通知,取消状态订阅。
NF发现:提供NF发现服务。
(图 3GPP TS 23.502-5.2.7.1)
2)NRF的工作过程也是CS模式,区别是:
5G网元使用HTTP2,Flask默认HTTP。
5G网元请求完收到数据交给后端处理,无浏览器。
5G网元使用REST API风格定义资源,和URL有区别。
3)虽然有以上3个明显的区别,但我们仍然可以利用Flask的能力来模拟网元的交互过程,加深对5G网元交互的理解。
4)另外,实际当中,除了23.502中列出的NRF支持的操作外,29.510提到NRF还支持:
获取实例集合,query是可选参数,用作查询条件。
获取某个实例的信息。
(图 3GPP TS 29.510 5.2.2)
6.获取NRF上的实例集合
1)首先简单看一下5G网元的API结构:(3GPP TS 29.501 4.4.1)
格式:{apiRoot}/<apiName>/<apiVersion>/<apiSpecificResourceUriPart>
案例:http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances
apiName:nnrf-nfm
apiVersion:v1
apiSpecificResourceUriPart:nf-instances
2)接下来让我们直接上手模拟查询NRF上的实例集合。给app.py添加如下内容:vi app.py
# 导入flask内嵌json模块,用于处理json数据和文件
from flask import Flask, json
# 封装一个加载json文件的函数,供其他函数调用。5G网元大部分数据都是通过JSON传递。
def load_file(file_name):
with open('5gc/%s.json' % file_name) as f:
data = json.loads(f.read())
return data
# NRF的获取实例集合的API
def list_instance():
# 这是一个提前写好的数据文件,里面存了一些实例样本
return load_file('ins') # 只传名称即可
3)把材料包中的5gc.zip解压到5gc.zip目录下:
节点实例一般以uuid命令,为方便区分,我把uuid改成了节点名称,如amf05.json。
4)访问路径:
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances
查到了10个节点的API
4)抓包看返回的数据是JSON:(实际从浏览器也能看出来)
7.按条件查询实例
1)前文说到nf-instances后面可以加筛选条件,参数如何构建呢?先来看一个URL示例:
格式:scheme://netloc/path;params?query#fragment
案例:http://www.baidu.com/index.html;user?id=5#comment
scheme:// :http://,协议
netloc:www.baidu.com,域名
path:/index.html,资源路径
params:user,参数
query:id=5,查询条件,多个查询条件用&割开
fragment:comment,锚点,用#与前面内容隔开
2)对应5G按条件查询就是:
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5&type=amf
这里num和type就是参数,&是多个参数的分隔符。
以上参数是我自定义的,实际网元并没有使用num和type作为查询条件。
3)用Flask实现如下,修改app.py:vi app.py
# 导入flask中的request包,它会自动封装客户的请求报文
from flask import Flask, json, request
def list_instance():
# args是request封装的客户端请求的查询参数字典
num = request.args.get('num')
# 如果查询参数num是5,返回ins5
if num == '5':
# ins5.json里只放了5个实例
return load_file('ins5')
else:
return load_file('ins')
4)电脑访问如下URL:
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5
5)感兴趣的可以直接:return request.args
def list_instance():
return request.args
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances?num=5&type=amf&date=093
8.查询某实例详细信息
1)通过查询集合只能得到实例API,并不能得到实例的详细信息,想查看实例的详细信息,需要单独发请求,如前面5个实例的API。
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/0c765084-9cc5-49c6-9876-ae2f5fa2a604
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/72a57feb-92b7-469d-92a2-babae9f8a7a3
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/f1bb352f-3cd4-4843-9125-f590d6ad8c7b
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/de9f9fd9-ff3f-4255-a635-51f2e11c92fd
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/13a1de33-ec45-4cd6-a842-ce5bb3cba3d9
观察发现这些API只有instances后面的内容不同(每节点有自己的UUID),是否有办法简单汇总成一个URL,然后交给一个函数处理?答案是可以的,现在请出URL规则中的变量。
2)在app.py中添加内容:vi app.py
# 为了方便区分文件,写一个uuid前缀映射成文件名的字典
# 如果不考虑区分,代码会简单更简单
uton = {
"f1bb352f-3cd4-4843-9125-f590d6ad8c7b": "amf03"
"60b08736-f384-46bd-b990-63b1a4f2a61c": "amf04"
"72a57feb-92b7-469d-92a2-babae9f8a7a3": "amf05"
"0c765084-9cc5-49c6-9876-ae2f5fa2a604": "ausf04"
"13a1de33-ec45-4cd6-a842-ce5bb3cba3d9": "nssf04"
"de9f9fd9-ff3f-4255-a635-51f2e11c92fd": "pcf02"
"fefd85ba-d52f-41e9-b3f3-100920000001": "smf03"
"5061c517-8ede-4c3a-a465-b151bd2e8e49": "smf04"
"123e4567-e89b-42d3-4456-426655440004": "udm04"
"bb2a33fd-5b16-4b86-9d14-249f12f45b93": "udr04"
}
# 写一个新URL规则,填入<变量>,即可在函数中使用
def get_instance(uuid):
if uuid in uton.keys():
return load_file(uton[uuid])
else:
# 如果uuid不在字典中,说明uuid错误,return后面可以自定义错误代码
# 不写默认200
return {'Message': 'Wrong instance id'}, 400
需要注意,以下2个URL规则是不同的,前者没有下一层的path'/',后者有,访问后一个不会被前一个匹配,多了一个'/'就不同。
/v1/nf-instances
/v1/nf-instances/<uuid>
3)各节点对应的instance id可以用以下命令查看:
grep -i nfInstanceId *
4)尝试查询节点信息:
http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/0c765084-9cc5-49c6-9876-ae2f5fa2a604
9.模拟节点注册
1)前文只是看已注册的节点,现在我们来模拟注册节点到NRF,这步需要Windows使用python爬虫提交json数据。
2)先看下3GPP的图:(图 3GPP TS 29.510 5.2.2.2)
节点注册用PUT方法,API最后是节uuid,成功NRF需要返回201。
3)因为注册和查看实例信息的URL规则相同,所以需要修改之前的查看实例函数:vi app.py
# 封装一个将数据保存为json文件的函数
def save_file(file_name, data):
with open('5gc/%s.json' % file_name, 'w') as f:
f.write(json.dumps(data, indent=2, ensure_ascii=False))
return 0
# 限制请求的方法
@app.route('/nnrf-nfm/v1/nf-instances/<uuid>', methods=['PUT', 'GET'])
def get_instance(uuid):
if request.method == 'GET':
if uuid in uton.keys():
return load_file(uton[uuid])
else:
return {'Message': 'Wrong instance id'}, 400
# 如果请求的方法是PUT,使用request内置的获取json方法获取其数据
elif request.method == 'PUT':
j_data = request.get_json()
save_file(uuid, j_data)
# 添加节点和文件名的映射字典中
uton[uuid] = uuid
return {'Message': 'Created.'}, 201
else:
return {'Message': 'Wrong method of request'}, 400
4)在自己的电脑里写个爬虫:simu_reg.py
from urllib import request
import json
url = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'
headers = {'user-agent': 'AMF', 'content-type': 'application/json'}
# 准备提交的数据,简单起见,我只写了一点amf相关信息
data = {
"amfInfo": "amf06",
"fqdn": "amf06.amf.5gc.mnc007.mcc460.3gppnetwork.org",
"ipv4Addresses": "192.102.1.8",
"nfInstanceId": "11111111-aaaa-bbbb-cccc-dddddddddddd",
"nfStatus": "REGISTERED",
"nfType": "AMF"
}
j_data = json.dumps(data)
b_data = bytes(j_data, encoding='utf-8')
req = request.Request(url, data=b_data, headers=headers, method='PUT')
r = request.urlopen(req)
print(r.read().decode('utf-8'))
5)先用浏览器查询一下,提示错误的instance id。
6)运行爬虫:成功注册
7)再次用浏览器查询能够查询到,说明注册成功
10.模拟节点更新
1)先看看3GPP中的更新流程:(图 TS 29.510 5.2.2.3.1)
规范中更新有2种方式:
PUT:完全替换更新
PATCH:部分替换更新
另外从API上看它和注册,是相同的,所以还是要修改的查询实例函数。
2)修改之前的查看实例函数:vi app.py
# 扩充PATCH方法
def get_instance(uuid):
# GET都是查询
if request.method == 'GET':
if uuid in uton.keys():
return load_file(uton[uuid])
else:
return {'Message': 'Wrong instance id'}, 400
# PUT有可能是更新,也可能是注册,所以需要判断一下在字典中有没有
elif request.method == 'PUT':
j_data = request.get_json()
save_file(uuid, j_data)
if uuid not in uton.keys():
uton[uuid] = uuid
return {'Message': 'Created'}
else:
return {'Message': 'Updated'}
# PATCH是部分更新
elif request.method == 'PATCH':
i_data = request.get_json()
if uuid in uton.keys():
# 加载历史数据,更换相应值
o_data = load_file(uton[uuid])
for item in i_data.keys():
o_data[item] = i_data[item]
save_file(uton[uuid], o_data)
return {'Message': 'Patched'}
else:
return {'Message': 'Your instance ID does not exist'}, 400
else:
return {'Message': 'Wrong request method'}, 400
3)windows电脑写2个脚本:simu_update.py
# update是整体更新,过程实际和注册相同,只是返回值是200,不是201。
from urllib import request
import json
url = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'
headers = {'user-agent': 'AMF', 'content-type': 'application/json'}
# 这里替换了nfstatus为暂停,相当于AMF通知NRF,AMF下线了。
data = {
"amfInfo": "amf06",
"fqdn": "amf06.amf.5gc.mnc007.mcc460.3gppnetwork.org",
"ipv4Addresses": "192.102.1.8",
"nfInstanceId": "11111111-aaaa-bbbb-cccc-dddddddddddd",
"nfStatus": "SUSPEND",
"nfType": "AMF"
}
j_data = json.dumps(data)
b_data = bytes(j_data, encoding='utf-8')
req = request.Request(url, data=b_data, headers=headers, method='PUT')
r = request.urlopen(req)
print(r.read().decode('utf-8'))
simu_patch.py
from urllib import request
import json
url = 'http://192.168.70.138:5000/nnrf-nfm/v1/nf-instances/11111111-aaaa-bbbb-cccc-dddddddddddd'
headers = {'user-agent': 'AMF', 'content-type': 'application/json'}
# 这里将状态再改回到REGISTERED
data = {"nfStatus": "REGISTERED"}
j_data = json.dumps(data)
b_data = bytes(j_data, encoding='utf-8')
req = request.Request(url, data=b_data, headers=headers, method='PATCH')
r = request.urlopen(req)
print(r.read().decode('utf-8'))
4)修改app.py后flask自动重加载,uton字典重置,刚刚新注册的节点需要重新注册,之后才能验证更新。先运行注册爬虫:simu_reg.py
5)运行:simu_update.py
6)再运行:simu_patch.py
7)检查文件变动效果:
11.模拟节点注销
1)图3GPP TS 29.510 5.2.2.4
节点注销用DELETE,API和查看实例等一样,所以又要改一下查看实例函数,成功回复代码204。
2)修改app.py如下:vi app.py
# 添加delete方法
'/nnrf-nfm/v1/nf-instances/<uuid>', methods=['PUT', 'GET', 'PATCH', 'DELETE']) .route(
def get_instance(uuid):
if request.method == 'GET':
if uuid in uton.keys():
return load_file(uton[uuid])
else:
return {'Message': 'Wrong instance id'}, 400
elif request.method == 'PUT':
j_data = request.get_json()
save_file(uuid, j_data)
if uuid not in uton.keys():
uton[uuid] = uuid
return {'Message': 'Created'}
else:
return {'Message': 'Updated'}
elif request.method == 'PATCH':
i_data = request.get_json()
if uuid in uton.keys():
o_data = load_file(uton[uuid])
for item in i_data.keys():
o_data[item] = i_data[item]
save_file(uton[uuid], o_data)
return {'Message': 'Patched'}
else:
return {'Message': 'Your instance ID does not exist'}, 400
# 当时删除的时候,删除字典的映射。实际文件还保留。
elif request.method == 'DELETE':
if uuid in uton.keys():
del uton[uuid]
# 回复204
return '', 204
else:
return {'Message': 'Your instance ID does not exist'}, 400
else:
return {'Message': 'Wrong request method'}, 400
3)运行效果
同样是需要先注册节点,再注销节点,不然会报错。
4)抓包看成功返回204:
12.模拟节点发现
1)5G网元注册到了NRF上之后,其他节点想要使用,需要向NRF发起服务查询(发现),因此节点想真正提供服务,发现也是重要的环节。
2)先看图 3GPP TS 29.510 5.3.2.2.2-1
使用GET,回复200,查询节点集合相似。
3)对于节点发现,3GPP TS 23.502 5.2.7.3.2描述必须有3个基本查询参数:
target-nf-type:目标节点类型
service-names:目前节点能提供的服务名称
requester-nf-type:查询者的节点类型
综上,flask使用request.args对象就基本可以满足需求了。
4)添加discovery_node函数:vi app.py(注意发现节点的API名称是nnrf-disc,3GPP TS 29.510 5.1-1)
# 导入随机模块
import random
# 写一个网元类型和网元的字典
tton = {
'AMF': ['amf03', 'amf04', 'amf05'],
# 'SMF': ['smf03', 'smf04'],
'AUSF': 'ausf04',
'UDM': 'udm04',
'PCF': 'pcf02',
'UDR': 'udr04',
'NSSF': 'nssf04'
}
# 节点发现URL的URL规则
def discovery_node():
# 获取请求中的参数target-nf-type给tt变量
tt = request.args.get('target-nf-type')
# 获取请求中的requester-nf-instance-fqdn给fqdn,这是个可选参数
fqdn = request.args.get('requester-nf-instance-fqdn')
# 如果查询AMF,随机返回一个AMF,
if tt == 'AMF':
node = random.choice(tton['AMF'])
return load_file(node)
# 如果查询SMF,根据映射关系返回
elif tt == 'SMF':
if fqdn.startswith('amf03'):
return load_file('smf03')
elif fqdn.startswith('amf05'):
return load_file('smf04')
# 如果是其他节点,根据字典返回
elif tt in tton.keys():
return load_file(tton[tt])
# 如果tt类型不对,返回目标节点类型错误
else:
return {'Message': "Wrong target nf type."}, 400
5)用浏览器当Client(作为SMF),查询2次AMF,返回了不同的AMF:
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF
6)查询SMF:(注意加了fqdn)
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=SMF&requester-nf-type=AMF&requester-nf-instance-fqdn=amf03
7)查询AUSF:
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AUSF&requester-nf-type=AMF
8)给一个错误的目标网元类型:
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AAA&requester-nf-type=SMF
9)需要说明的是,本文档只是演示交互过程,实际节点查询,会考虑多个参数,下面是一个真实SMF查询AMF的URL:
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF&requester-nf-instance-fqdn=smf04.er.pc.smf.5gc.mnc008.mcc460.3gppnetwork.org&guami=%7B%22plmnId%22%3A%7B%22mcc%22%3A%20%22460%22%2C%20%22mnc%22%3A%20%2208%22%7D%2c%20%22amfId%22%3A%20%22$ID_X%22%7D"
以上是URL编码之后的内容,解码后内容为:
http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF&requester-nf-instance-fqdn=smf04.er.pc.smf.5gc.mnc008.mcc460.3gppnetwork.org&guami={"plmnId":{"mcc": "460", "mnc": "08"}, "amfId": "7"}"
可以看到,不仅有3个基本参数,还有其他参数,只有参数正确,NRF才能返回正确的结果。
10)另外windows用爬虫发现节点:
from urllib import request
url = 'http://192.168.70.138:5000/nnrf-disc/v1/nf-instances?service-names=namf-comm&target-nf-type=AMF&requester-nf-type=SMF'
headers = {'user-agent': 'python'}
req = request.Request(url, headers=headers)
r = request.urlopen(req)
print(r.read().decode('utf-8'))
13.总结
Flask上手简单,但需要一些Python基础。
修改配置后未永久保存的数据会丢失,可考虑使用pickle永久存储。
模拟交互需要注意HTTP 方法的逻辑,必要时画逻辑图。
实际Flask还可以模拟UDM/AUSF,不过方法类似,本文就不做展示了。
以上是关于Flask模拟NRF的主要内容,如果未能解决你的问题,请参考以下文章
《安富莱嵌入式周报》第279期:强劲的代码片段搜索工具,卡内基梅隆大学安全可靠C编码标准,Nordic发布双频WiFi6 nRF7002芯片
[异常解决] Make nRF51 DFU Project Appear "fatal error: uECC.h: No such file or directory"(代码片段
蓝牙5系列SoC芯片NRF52820/NRF52840替代nrf52833