Open
feat: 支持snmp组件使用cpp版本映射器 #9
ouwenjianAdd on Aug 31
feat: 支持snmp组件使用cpp版本映射器 #9
Open
ouwenjianAdd on Aug 31
Total: 11 files changed+670-442
MREADME.md+39-2
@@ -86,7 +86,7 @@ SNMP Manager ──SNMP v1/v2c/v3──> net-snmp Agent(snmpd) ──> libsnmp.s
86# 遍历本组件注册的 SNMP 接口(OEM OID 子树)86# 遍历本组件注册的 SNMP 接口(OEM OID 子树)
87snmpwalk -v2c -c <community> <bmc_ip> 1.3.6.1.4.1.2011.2.235.1.187snmpwalk -v2c -c <community> <bmc_ip> 1.3.6.1.4.1.2011.2.235.1.1
88 88 
89-# 读取标量接口(OID 后追加 .0;示例 trapEnable,见 test/unit/data/mapping_config/config.lua)89+# 读取标量接口(OID 后追加 .0;示例 trapEnable,路由样例见 test/unit/mock_routemapper.lua)
90snmpget -v2c -c <community> <bmc_ip> 1.3.6.1.4.1.2011.2.235.1.1.4.1.090snmpget -v2c -c <community> <bmc_ip> 1.3.6.1.4.1.2011.2.235.1.1.4.1.0
91# 示意响应(需环境验证):iso.3.6.1.4.1.2011.2.235.1.1.4.1.0 = INTEGER: 191# 示意响应(需环境验证):iso.3.6.1.4.1.2011.2.235.1.1.4.1.0 = INTEGER: 1
92 92 
@@ -247,7 +247,7 @@ Received SNMP packet(s) from UDP: [<客户端IP>]:<端口>. oid:<OID>
247 247 
248**Q2:新增 SNMP 接口需要改代码吗?**248**Q2:新增 SNMP 接口需要改代码吗?**
249一句话答案:不需要,在映射配置目录新增 URI 配置即可。249一句话答案:不需要,在映射配置目录新增 URI 配置即可。
250-根因:组件经 `route_mapper` 读取 `interface_config/<product>/mapping_config` 动态注册。250+根因:组件经进程内路由映射器读取 `interface_config/<product>` 配置根(`mapping_config`/`script`/`plugins`)动态注册。
251解决方案:按 [3](#3-组件扩展案例) 示例配置,注意 URI 校验规则,重启 snmpd 后用 `snmpwalk`/`snmpget` 验证。251解决方案:按 [3](#3-组件扩展案例) 示例配置,注意 URI 校验规则,重启 snmpd 后用 `snmpwalk`/`snmpget` 验证。
252 252 
253**Q3:表接口 `snmpwalk` 遍历不到数据?**253**Q3:表接口 `snmpwalk` 遍历不到数据?**
@@ -264,3 +264,40 @@ Received SNMP packet(s) from UDP: [<客户端IP>]:<端口>. oid:<OID>
264一句话答案:通过替换关系配置 `SnmpOemIdentifier` 设置。264一句话答案:通过替换关系配置 `SnmpOemIdentifier` 设置。
265根因:默认 OEM 前缀 `1.3.6.1.4.1.2011.2.235.1.1.`,组件会替换为 `1.3.6.1.4.1.<替换值>.`。265根因:默认 OEM 前缀 `1.3.6.1.4.1.2011.2.235.1.1.`,组件会替换为 `1.3.6.1.4.1.<替换值>.`。
266解决方案:设置 `SnmpOemIdentifier` 并保证映射配置 OID 前缀一致。266解决方案:设置 `SnmpOemIdentifier` 并保证映射配置 OID 前缀一致。
267+ 
268+---
269+ 
270+## 7. 映射器机制(进程内路由映射器)
271+ 
272+snmp 已接入新版「进程内路由映射器」(libroute_mapper + libmcpp 提供的路由映射机制):不再依赖旧的
273+`route_mapper.*` / `libroutemapper.*` 运行库与独立 worker 服务,而是在 `snmpd` 进程内直接创建路由映射器实例,
274+复用组件已有的 SDBus(同步阻塞模式)完成 MDB、认证、策略与 Task 请求。
275+ 
276+**接入内容**:
277+ 
278+- `src/lualib/snmp_mapper.lua`:SNMP 侧映射器门面。基于 `routemapper.sync` 在进程内加载映射配置,
279+ 提供 `execute` / `describe` / `list_routes` / `reload` 等能力,并把映射器错误统一规范化为 SNMP 错误消息
280+ (`MessageName` / `Message` / `SnmpStatusCode`)。snmp 进程没有 skynet 调度器,映射器调用统一走
281+ `*_async` 接口 + `mc.actor.step()` 驱动循环 + `take_ready` 的同步等待模式(`await`),
282+ 等待期间驱动本 VM 的 actor 队列以执行 Script/Plugin 等扩展回调。
283+- `src/lualib/config.lua`:通过 `list_routes()` + `describe()`(编译后的公开元数据)构建接口资源表,
284+ 不再访问映射器内部路由表。
285+- `src/lualib/interface.lua`、`task_mgmt.lua`:请求执行改为 `mapper:execute()`,任务信息取自执行结果
286+ `effects.Tasks`,不再依赖旧 `mapper:match()` 返回的 `extra_info`。
287+- 通用 MDB 访问、`SnmpOemIdentifier` 占位符替换(`{{SnmpOemIdentifier}}`)由映射器内部完成;
288+ 组件自身读取 `config.json` 的 `SnmpOemIdentifier` 仅用于 URI 校验。
289+ 
290+**配置根目录语义**:进程内路由映射器的 `load()` 接收部署配置根目录(`mapping_config`、`script`、`plugins`
291+的公共父目录),即 `/opt/bmc/apps/snmp/interface_config[/<platform_id>_<board_id>]`,不再传 `mapping_config`
292+子目录。
293+ 
294+**运行依赖**:设备上需要安装与目标工具链匹配的新版 `libroute_mapper` 与 `libmcpp` 运行包,至少包括:
295+ 
296+- `/usr/lib64/libroute_mapper_runtime.so`;
297+- `/usr/lib64/routemapper/core.so`;
298+- `/usr/share/lua/routemapper/init.lua`、`runtime.lua`、`host.lua`、`sync.lua`、`sdbus_transport.lua`(及 `skynet.lua`、`cjson.lua`);
299+- 底层 `libmcpp` 运行库(mcbase/mcengine/mcapp/mcdbus/lmc 等)。
300+ 
301+**验证**:启动 snmpd 后日志应出现 `initialize in-process route mapper successfully`,且不再加载旧的
302+`route_mapper.worker_service`;单元测试 `test/unit` 通过 mock 路由映射器门面(`test/unit/mock_routemapper.lua`)
303+校验接口注册、请求体、主键与 Sequence 约束。
@@ -1,7 +1,7 @@
1{1{
2 "name": "snmp",2 "name": "snmp",
3 "type": "library",3 "type": "library",
4- "version": "1.120.5",4+ "version": "1.120.6",
5 "author": "Huawei",5 "author": "Huawei",
6 "license": "Mulan PSL v2",6 "license": "Mulan PSL v2",
7 "dependencies": {7 "dependencies": {
@@ -13,6 +13,10 @@ local log = require('mc.logging')
13local innerbus_ok, innerbus_drv = pcall(require, 'libsoc_adapter.innerbus')13local innerbus_ok, innerbus_drv = pcall(require, 'libsoc_adapter.innerbus')
14innerbus_drv = innerbus_ok and innerbus_drv or {}14innerbus_drv = innerbus_ok and innerbus_drv or {}
15local utils_core = require('utils.core')15local utils_core = require('utils.core')
16+local file_sec_ok, file_sec = pcall(require, 'utils.file')
17+file_sec = file_sec_ok and file_sec or nil
18+local cjson = require('cjson')
19+local snmp_mapper_ok = pcall(require, 'snmp_mapper')
16 20 
17local m = class()21local m = class()
18 22 
@@ -28,9 +32,15 @@ function m:init()
28end32end
29 33 
30local function is_task_get_interface(res)34local function is_task_get_interface(res)
35+ local methods
36+ if not snmp_mapper_ok then
37+ methods = res.methods
38+ else
39+ methods = res
40+ end
31 local ok, ret = pcall(function()41 local ok, ret = pcall(function()
32 local method_cnt = 042 local method_cnt = 0
33- for _ in pairs(res.methods) do43+ for _ in pairs(methods) do
34 method_cnt = method_cnt + 144 method_cnt = method_cnt + 1
35 end45 end
36 46 
@@ -38,7 +48,7 @@ local function is_task_get_interface(res)
38 return false48 return false
39 end49 end
40 50 
41- local interface = res.methods.get51+ local interface = methods.get
42 if not interface then52 if not interface then
43 return false53 return false
44 end54 end
@@ -58,16 +68,104 @@ local function is_task_get_interface(res)
58 return ret68 return ret
59end69end
60 70 
71+-- 从编译后的端点描述中提取请求体配置(与原始 ReqBody 结构一致)
72+local function extract_reqbody(description)
73+ return description.RequestSchema or nil
74+end
75+ 
76+-- 从编译后的响应字段构建默认响应体 {Name = Default}
77+local function extract_default_rsp_body(description)
78+ local rsp_body = {}
79+ for _, field in ipairs(description.ResponseFields or {}) do
80+ rsp_body[field.Name] = field.Default
81+ end
82+ 
83+ return rsp_body
84+end
85+ 
86+-- 用编译后的端点描述构建一个方法的接口配置
87+function m:describe_interface(uri, method, description)
88+ description = description or self.mapper:describe(uri, method, {})
89+ if type(description) ~= 'table' then
90+ return nil
91+ end
92+ 
93+ local intf = {}
94+ if description.TaskReferenceUri and #description.TaskReferenceUri ~= 0 then
95+ intf.TaskReferenceUri = description.TaskReferenceUri
96+ end
97+ 
98+ intf.ReqBody = extract_reqbody(description)
99+ intf.RspBody = extract_default_rsp_body(description)
100+ 
101+ return intf
102+end
103+ 
61function m:load_mapping()104function m:load_mapping()
62- if not (self.mapper and self.mapper.routers) then105+ local configuration_missing
106+ if snmp_mapper_ok then
107+ configuration_missing = not self.mapper
108+ else
109+ configuration_missing = not (self.mapper and self.mapper.routers)
110+ end
111+ if configuration_missing then
63 log:error('The route configuration is missing.')112 log:error('The route configuration is missing.')
64 return113 return
65 end114 end
66 115 
67- for uri, config in pairs(self.mapper.routers) do116+ if not snmp_mapper_ok then
68- config.methods.IsTaskGet = is_task_get_interface(config)117+ for uri, config in pairs(self.mapper.routers) do
69- self.resources[uri] = config.methods118+ config.methods.IsTaskGet = is_task_get_interface(config)
70- self.resources[uri].Sequence = config.Sequence119+ self.resources[uri] = config.methods
120+ self.resources[uri].Sequence = config.Sequence
121+ end
122+ return
123+ end
124+ 
125+ local route_infos = self.mapper:list_routes()
126+ if type(route_infos) ~= 'table' then
127+ log:error('The route configuration is missing.')
128+ return
129+ end
130+ 
131+ for _, route_info in ipairs(route_infos) do
132+ local uri = route_info.Uri
133+ if not uri then
134+ goto continue
135+ end
136+ 
137+ -- 以 GET 描述为准填充 Sequence 与任务标识
138+ local get_description = self.mapper:describe(uri, 'get', {})
139+ local resource = {}
140+ 
141+ for _, method in ipairs(route_info.Methods or {}) do
142+ local lower_method = string.lower(method)
143+ local intf
144+ if lower_method == 'get' then
145+ intf = self:describe_interface(uri, method, get_description)
146+ else
147+ intf = self:describe_interface(uri, method)
148+ end
149+ 
150+ if intf then
151+ resource[lower_method] = intf
152+ end
153+ end
154+ 
155+ if next(resource) == nil then
156+ goto continue
157+ end
158+ 
159+ -- 任务判断需在填充 Sequence 之前进行(避免把 Sequence 误算为方法)
160+ resource.IsTaskGet = is_task_get_interface(resource)
161+ 
162+ if type(get_description) == 'table' and get_description.Sequence and #get_description.Sequence ~= 0 then
163+ resource.Sequence = get_description.Sequence
164+ end
165+ 
166+ self.resources[uri] = resource
167+ 
168+ ::continue::
71 end169 end
72end170end
73 171 
@@ -106,4 +204,38 @@ function m.get_interface_config_path()
106 return intf_path204 return intf_path
107end205end
108 206 
207+-- 读取 config.json 中的 OEM OID 变量,用于校验映射配置中的 OID 前缀
208+function m.get_snmp_oem_identifier()
209+ if not file_sec then
210+ return nil
211+ end
212+ 
213+ local config_path = m.get_interface_config_path() .. '/config.json'
214+ local file = file_sec.open_s(config_path, 'r')
215+ if not file then
216+ log:info('open %s failed.', config_path)
217+ return nil
218+ end
219+ 
220+ local ok, content = pcall(function()
221+ return file:read('*all')
222+ end)
223+ file:close()
224+ if not ok or not content then
225+ return nil
226+ end
227+ 
228+ local is_ok, config = pcall(cjson.json_object_ordered_decode, content)
229+ if not is_ok or not config or not config.GlobalVariable then
230+ return nil
231+ end
232+ 
233+ local identifier = config.GlobalVariable.SnmpOemIdentifier
234+ if type(identifier) ~= 'string' or identifier == '' then
235+ return nil
236+ end
237+ 
238+ return identifier
239+end
240+ 
109return m241return m
@@ -15,14 +15,18 @@ local utils = require('mc.utils')
15local utils_vos = require('utils.vos')15local utils_vos = require('utils.vos')
16local json = require('cjson')16local json = require('cjson')
17local messsage = require('message')17local messsage = require('message')
18-local c_mapper = require('route_mapper.route_mapper')18+local _, c_mapper = pcall(require, 'route_mapper.route_mapper')
19local c_config = require('config')19local c_config = require('config')
20local c_task_mgmt = require('task_mgmt')20local c_task_mgmt = require('task_mgmt')
21-local mapper_intf = require('route_mapper.interface')21+local _, mapper_intf = pcall(require, 'route_mapper.interface')
22local context = require('mc.context')22local context = require('mc.context')
23local traces = require('telemetry.trace')23local traces = require('telemetry.trace')
24local tracer = traces.get_tracer('snmp')24local tracer = traces.get_tracer('snmp')
25local telemetry = require('telemetry')25local telemetry = require('telemetry')
26+local cpp_mapper_ok, cpp_mapper = pcall(require, 'snmp_mapper')
27+if cpp_mapper_ok then
28+ c_mapper = cpp_mapper
29+end
26 30 
27local SNMP_URI_PREFIX <const> = '/snmp/'31local SNMP_URI_PREFIX <const> = '/snmp/'
28local MDB_PATH_PREFIX <const> = '/bmc/kepler/'32local MDB_PATH_PREFIX <const> = '/bmc/kepler/'
@@ -94,26 +98,51 @@ function m:interface_match(uri, method, address, username, reqbody)
94 if self.task_mgmt:is_task_get_interface(uri) then98 if self.task_mgmt:is_task_get_interface(uri) then
95 local task_uri = self.task_mgmt:get_task_uri(uri)99 local task_uri = self.task_mgmt:get_task_uri(uri)
96 if not task_uri then100 if not task_uri then
97- return nil, self.task_mgmt:get_default_task_info(uri)101+ if not cpp_mapper_ok then
102+ return nil, self.task_mgmt:get_default_task_info(uri)
103+ else
104+ return {
105+ body = self.task_mgmt:get_default_task_info(uri),
106+ errors = {},
107+ effects = {},
108+ }
109+ end
98 end110 end
99 111 
100 uri = task_uri112 uri = task_uri
101 end113 end
102 114 
103 local body = reqbody and json.json_object_ordered_decode(reqbody)115 local body = reqbody and json.json_object_ordered_decode(reqbody)
104- return self.mapper:match(uri, method, {116+ if not cpp_mapper_ok then
105- body = body,117+ return self.mapper:match(uri, method, {
106- user = {118+ body = body,
107- Interface = 'SNMP',119+ user = {
108- ClientIp = address,120+ Interface = 'SNMP',
109- UserName = username,121+ ClientIp = address,
110- },122+ UserName = username,
111- })123+ },
124+ })
125+ else
126+ return self.mapper:execute(uri, method, {
127+ body = body,
128+ user = {
129+ Interface = 'SNMP',
130+ ClientIp = address,
131+ UserName = username,
132+ },
133+ })
134+ end
112 end)135 end)
113 if not ok then136 if not ok then
114 return messsage.SNMP_ERR_GENERR, 'string', errs137 return messsage.SNMP_ERR_GENERR, 'string', errs
115 end138 end
116 139 
140+ if cpp_mapper_ok and errs then
141+ rsp_body = errs.body
142+ extra_info = errs.effects
143+ errs = errs.errors
144+ end
145+ 
117 if errs and #errs ~= 0 then146 if errs and #errs ~= 0 then
118 local err_code, err_message = messsage.parse_error_message(errs)147 local err_code, err_message = messsage.parse_error_message(errs)
119 if err_code == messsage.SNMP_ERR_NOERROR then148 if err_code == messsage.SNMP_ERR_NOERROR then
@@ -228,7 +257,17 @@ function m:parse_interface_response(uri, rsp_body, task_info)
228 local span = tracer:start_span('snmp_parse_interface_response', { uri = uri }, { kind = 'Server' })257 local span = tracer:start_span('snmp_parse_interface_response', { uri = uri }, { kind = 'Server' })
229 -- 解析Get接口返回值258 -- 解析Get接口返回值
230 local ok, err = pcall(function()259 local ok, err = pcall(function()
231- if task_info and task_info.TaskInfo and task_info.TaskInfo.TaskId > 0 and task_info.TaskInfo.Path then260+ local need_create_new_task
261+ if not cpp_mapper_ok then
262+ need_create_new_task = task_info
263+ and task_info.TaskInfo
264+ and task_info.TaskInfo.TaskId > 0
265+ and task_info.TaskInfo.Path
266+ else
267+ task_info = task_info and task_info.Tasks and task_info.Tasks[1]
268+ need_create_new_task = task_info and task_info.TaskId > 0 and task_info.Path
269+ end
270+ if need_create_new_task then
232 self.task_mgmt:create_new_task(uri, task_info)271 self.task_mgmt:create_new_task(uri, task_info)
233 end272 end
234 273 
@@ -276,9 +315,16 @@ function m:get_all_interface_uri_config()
276 local data = {}315 local data = {}
277 316 
278 local ibmc_prefix = IBMC_OID_PREFIX317 local ibmc_prefix = IBMC_OID_PREFIX
279- local replace_relation = mapper_intf.get_replace_relation(self.bus, c_config.get_interface_config_path())318+ if not cpp_mapper_ok then
280- if replace_relation ~= nil and replace_relation['SnmpOemIdentifier'] ~= nil then319+ local replace_relation = mapper_intf.get_replace_relation(self.bus, c_config.get_interface_config_path())
281- ibmc_prefix = '1.3.6.1.4.1.' .. replace_relation['SnmpOemIdentifier'] .. '.'320+ if replace_relation ~= nil and replace_relation['SnmpOemIdentifier'] ~= nil then
321+ ibmc_prefix = '1.3.6.1.4.1.' .. replace_relation['SnmpOemIdentifier'] .. '.'
322+ end
323+ else
324+ local snmp_oem_identifier = c_config.get_snmp_oem_identifier()
325+ if snmp_oem_identifier ~= nil then
326+ ibmc_prefix = '1.3.6.1.4.1.' .. snmp_oem_identifier .. '.'
327+ end
282 end328 end
283 329 
284 for uri, _ in pairs(self.config.resources) do330 for uri, _ in pairs(self.config.resources) do
@@ -0,0 +1,169 @@
1+-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+-- openUBMC is licensed under Mulan PSL v2.
3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4+-- You may obtain a copy of Mulan PSL v2 at:
5+-- http://license.coscl.org.cn/MulanPSL2
6+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+-- See the Mulan PSL v2 for more details.
10+ 
11+-- Description: SNMP facade for the in-process route mapper
12+ 
13+local class = require('mc.class')
14+local log = require('mc.logging')
15+local cjson = require('cjson')
16+local mc = require('routemapper.runtime')
17+local sync_mapper = require('routemapper.sync')
18+local utils_vos = require('utils.vos')
19+local message = require('message')
20+ 
21+local mapper_facade = class()
22+ 
23+-- 同步等待路由映射器的异步结果。
24+-- snmp 进程没有 skynet 调度器,无法直接调用可 yield 的同步方法(future 未就绪时
25+-- 会在 lmc 的 yieldable 守卫处报错),因此改用 *_async 接口 + take_ready:
26+-- 等待期间驱动本 VM 的 actor 队列,执行 Script/Plugin 等扩展回调推进 future。
27+local AWAIT_TIMEOUT_MS = 15000
28+ 
29+local function await(native, future)
30+ local begin = utils_vos.vos_tick_get()
31+ while not future:is_ready() do
32+ local progressed = mc.actor.step()
33+ if progressed == 0 and utils_vos.vos_tick_get() - begin >= AWAIT_TIMEOUT_MS then
34+ error('route mapper future await timeout', 0)
35+ end
36+ end
37+ 
38+ mc.actor.step()
39+ 
40+ return native:take_ready(future)
41+end
42+ 
43+-- 把 sd_bus 包装成 route mapper transport 需要的对象:
44+-- 必须提供 bus:timeout_call(timeout_ms, destination, path, interface, member, signature, ...)
45+local function make_bus_adapter(sd_bus_obj)
46+ local adapter = {}
47+ 
48+ function adapter:timeout_call(timeout_ms, destination, path, interface, member, signature, ...)
49+ return sd_bus_obj.bus:timeout_call(timeout_ms, destination, path, interface, member, signature, ...)
50+ end
51+ 
52+ return adapter
53+end
54+ 
55+local function to_plain_value(value)
56+ if type(value) ~= 'userdata' then
57+ return value
58+ end
59+ 
60+ if cjson.json_object_is_object(value) or cjson.json_object_is_array(value) then
61+ return cjson.json_object_to_table(value)
62+ end
63+ 
64+ return value
65+end
66+ 
67+-- execute 返回的 body/action_body 是 C++ 侧按映射配置插入序序列化的 JSON
68+-- 字符串,直接保序解码,避免 Lua 表键序哈希化;SNMP 消费端按 table 消费。
69+-- 纯文本响应(非 JSON)解码失败时原样返回字符串。
70+local function to_snmp_value(value)
71+ if type(value) == 'string' then
72+ local ok, decoded = pcall(cjson.json_object_ordered_decode, value)
73+ if ok then
74+ return decoded
75+ end
76+ return value
77+ end
78+ if type(value) ~= 'table' then
79+ return value
80+ end
81+ 
82+ return cjson.json_object_ordered_decode(cjson.encode(value))
83+end
84+ 
85+-- 把 route mapper 的错误统一转换为 SNMP 错误消息结构
86+local function normalize_error(err)
87+ if type(err) ~= 'table' then
88+ err = {}
89+ end
90+ 
91+ local name = err.MessageName or err.Code or err.Name
92+ if type(name) ~= 'string' or name == '' then
93+ name = 'InternalError'
94+ end
95+ 
96+ return {
97+ MessageName = name,
98+ Message = err.Message or name,
99+ SnmpStatusCode = err.SnmpStatusCode or message.SNMP_ERR_GENERR,
100+ }
101+end
102+ 
103+local function normalize_errors(errors)
104+ local result = {}
105+ for index, err in ipairs(errors or {}) do
106+ result[index] = normalize_error(err)
107+ end
108+ return result
109+end
110+ 
111+function mapper_facade:ctor(config_root, bus)
112+ self.config_root = config_root
113+ self.bus = make_bus_adapter(bus)
114+ self.mapper = nil
115+end
116+ 
117+function mapper_facade:init()
118+ log:notice('initialize in-process route mapper: compile configuration')
119+ -- customer 为空时不加载 customer 目录,使用默认配置
120+ self.mapper = sync_mapper.load(self.config_root, self.bus, '', false, 'snmp')
121+ log:notice('initialize in-process route mapper successfully, generation=%d', self.mapper:generation())
122+end
123+ 
124+function mapper_facade:execute(uri, method, params)
125+ params = params or {}
126+ 
127+ local future = self.mapper:execute_async(
128+ string.upper(method),
129+ uri,
130+ to_plain_value(params.body),
131+ to_plain_value(params.header),
132+ to_plain_value(params.user),
133+ to_plain_value(params.query)
134+ )
135+ local result = await(self.mapper, future)
136+ result.body = to_snmp_value(result.body)
137+ result.errors = normalize_errors(result.errors)
138+ 
139+ return result
140+end
141+ 
142+function mapper_facade:describe(uri, method, params)
143+ params = params or {}
144+ 
145+ local future = self.mapper:describe_async(
146+ string.upper(method),
147+ uri,
148+ to_plain_value(params.body),
149+ to_plain_value(params.header),
150+ to_plain_value(params.user),
151+ to_plain_value(params.query)
152+ )
153+ 
154+ return await(self.mapper, future)
155+end
156+ 
157+function mapper_facade:list_routes()
158+ return self.mapper:list_routes()
159+end
160+ 
161+function mapper_facade:generation()
162+ return self.mapper:generation()
163+end
164+ 
165+function mapper_facade:reload()
166+ return self.mapper:reload(self.config_root, '')
167+end
168+ 
169+return mapper_facade
@@ -11,6 +11,7 @@
11local class = require('mc.class')11local class = require('mc.class')
12local log = require('mc.logging')12local log = require('mc.logging')
13local cjson = require('cjson')13local cjson = require('cjson')
14+local cpp_mapper_ok = pcall(require, 'snmp_mapper')
14 15 
15local TASK_PATH <const> = '%s/TaskService/Tasks/%d'16local TASK_PATH <const> = '%s/TaskService/Tasks/%d'
16 17 
@@ -25,8 +26,24 @@ end
25 26 
26function m:create_new_task(uri, task_info)27function m:create_new_task(uri, task_info)
27 self.task_index = self.task_index + 128 self.task_index = self.task_index + 1
29+ local task_path
30+ if not cpp_mapper_ok then
31+ task_path = string.format(TASK_PATH, task_info.TaskInfo.Path, task_info.TaskInfo.TaskId)
32+ else
33+ -- 任务信息来自映射器执行结果 effects.Tasks[1],
34+ -- Path 为任务资源路径,兼容是否包含 /TaskService/Tasks 两种形态
35+ task_path = task_info.Path
36+ if task_info.TaskId and tostring(task_info.TaskId) ~= '' then
37+ if string.match(task_path, '/TaskService/Tasks$') then
38+ task_path = task_path .. '/' .. tostring(task_info.TaskId)
39+ else
40+ task_path = string.format(TASK_PATH, task_path, task_info.TaskId)
41+ end
42+ end
43+ end
44+ 
28 self.task_list[uri] = {45 self.task_list[uri] = {
29- Path = string.format(TASK_PATH, task_info.TaskInfo.Path, task_info.TaskInfo.TaskId),46+ Path = task_path,
30 Index = self.task_index,47 Index = self.task_index,
31 }48 }
32end49end
@@ -24,7 +24,14 @@ log:set_log_module_name('snmp')
24local c_interface = require('interface')24local c_interface = require('interface')
25local config = require('config')25local config = require('config')
26 26 
27-local MAP_CONFIG_PATH = config.get_interface_config_path() .. '/mapping_config'27+local cpp_mapper_ok = pcall(require, 'snmp_mapper')
28+local MAP_CONFIG_PATH
29+if not cpp_mapper_ok then
30+ MAP_CONFIG_PATH = config.get_interface_config_path() .. '/mapping_config'
31+else
32+ -- 进程内路由映射器的配置根目录(mapping_config/script/plugins 的公共父目录)
33+ MAP_CONFIG_PATH = config.get_interface_config_path()
34+end
28 35 
29local interface = c_interface.new(MAP_CONFIG_PATH, true)36local interface = c_interface.new(MAP_CONFIG_PATH, true)
30 37 
@@ -1,384 +0,0 @@
1--- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2--- openUBMC is licensed under Mulan PSL v2.
3--- You can use this software according to the terms and conditions of the Mulan PSL v2.
4--- You may obtain a copy of Mulan PSL v2 at:
5--- http://license.coscl.org.cn/MulanPSL2
6--- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7--- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8--- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9--- See the Mulan PSL v2 for more details.
10- 
11-Input.data = {
12- ['/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.1/trapEnable/Readwrite'] = {
13- methods = {
14- get = {
15- RspBody = {
16- ['Enabled'] = '${Statements/Enabled()}',
17- },
18- Statements = {
19- ['Enabled'] = {
20- ['Input'] = '${ProcessingFlow[1]/Destination/Enabled}',
21- ['Steps'] = {
22- [1] = {
23- ['Type'] = 'Switch',
24- ['Formula'] = {
25- [1] = {
26- ['Case'] = false,
27- ['To'] = 1,
28- },
29- [2] = {
30- ['Case'] = true,
31- ['To'] = 2,
32- },
33- },
34- },
35- },
36- },
37- },
38- ProcessingFlow = {
39- [1] = {
40- ['Type'] = 'Property',
41- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp',
42- ['Interface'] = 'bmc.kepler.EventService.Subscriptions.Snmp',
43- ['Destination'] = {
44- ['Enabled'] = 'Enabled',
45- },
46- },
47- },
48- },
49- patch = {
50- ReqBody = {
51- ['Type'] = 'object',
52- ['Required'] = true,
53- ['Properties'] = {
54- ['Enabled'] = {
55- ['Required'] = true,
56- ['Type'] = 'integer',
57- ['Validator'] = {
58- [1] = {
59- ['Type'] = 'Enum',
60- ['Formula'] = {
61- [1] = 1,
62- [2] = 2,
63- },
64- },
65- },
66- },
67- },
68- },
69- Statements = {
70- ['Enabled'] = {
71- ['Input'] = '${ReqBody/Enabled}',
72- ['Steps'] = {
73- [1] = {
74- ['Type'] = 'Switch',
75- ['Formula'] = {
76- [1] = {
77- ['Case'] = 1,
78- ['To'] = false,
79- },
80- [2] = {
81- ['Case'] = 2,
82- ['To'] = true,
83- },
84- },
85- },
86- },
87- },
88- },
89- ProcessingFlow = {
90- [1] = {
91- ['Type'] = 'Property',
92- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp',
93- ['Interface'] = 'bmc.kepler.EventService.Subscriptions.Snmp',
94- ['Source'] = {
95- ['Enabled'] = '${Statements/Enabled()}',
96- },
97- },
98- },
99- },
100- },
101- },
102- ['/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.50/trapInfoDescriptionTable/Readwrite'] = {
103- Sequence = {
104- [1] = {
105- ['Name'] = 'trapReceiverIndex',
106- ['Type'] = 'integer',
107- ['Access'] = 'Readonly',
108- ['Primary'] = true,
109- },
110- [2] = {
111- ['Name'] = 'trapReceiverEnable',
112- ['Type'] = 'integer',
113- ['Access'] = 'Readwrite',
114- },
115- [3] = {
116- ['Name'] = 'trapReceiverAddress',
117- ['Type'] = 'string',
118- ['Access'] = 'Readwrite',
119- },
120- [4] = {
121- ['Name'] = 'trapReceiverPort',
122- ['Type'] = 'integer',
123- ['Access'] = 'Readwrite',
124- },
125- [5] = {
126- ['Name'] = 'trapSendType',
127- ['Type'] = 'integer',
128- ['Access'] = 'Readwrite',
129- },
130- [6] = {
131- ['Name'] = 'trapTest',
132- ['Type'] = 'string',
133- ['Access'] = 'Setonly',
134- },
135- [7] = {
136- ['Name'] = 'trapBobEnable',
137- ['Type'] = 'integer',
138- ['Access'] = 'Readwrite',
139- },
140- },
141- methods = {
142- get = {
143- RspBody = {
144- ['Sequence'] = '${Statements/TrapServer()}',
145- },
146- Statements = {
147- ['TrapServer'] = {
148- ['Input'] = '${ProcessingFlow[1]/Destination/TrapServer}',
149- ['Steps'] = {
150- [1] = {
151- ['Type'] = 'Expand',
152- ['Formula'] = '1',
153- },
154- },
155- },
156- },
157- ProcessingFlow = {
158- [1] = {
159- ['Type'] = 'List',
160- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp/Nmses',
161- ['Destination'] = {
162- ['Members'] = 'TrapServer',
163- },
164- },
165- },
166- },
167- patch = {
168- ReqBody = {
169- ['Type'] = 'object',
170- ['Required'] = true,
171- ['Properties'] = {
172- ['trapReceiverIndex'] = {
173- ['Type'] = 'integer',
174- ['Required'] = true,
175- ['Validator'] = {
176- [1] = {
177- ['Type'] = 'Range',
178- ['Formula'] = {
179- [1] = 1,
180- [2] = 4,
181- },
182- },
183- },
184- },
185- ['trapReceiverEnable'] = {
186- ['Type'] = 'integer',
187- ['Validator'] = {
188- [1] = {
189- ['Type'] = 'Enum',
190- ['Formula'] = {
191- [1] = 1,
192- [2] = 2,
193- },
194- },
195- },
196- },
197- ['trapReceiverAddress'] = {
198- ['Type'] = 'string',
199- },
200- ['trapReceiverPort'] = {
201- ['Type'] = 'integer',
202- ['Validator'] = {
203- [1] = {
204- ['Type'] = 'Range',
205- ['Formula'] = {
206- [1] = 1,
207- [2] = 65535,
208- },
209- },
210- },
211- },
212- ['trapSendType'] = {
213- ['Type'] = 'integer',
214- ['Validator'] = {
215- [1] = {
216- ['Type'] = 'Enum',
217- ['Formula'] = {
218- [1] = 1,
219- },
220- },
221- },
222- },
223- ['trapTest'] = {
224- ['Type'] = 'integer',
225- ['Validator'] = {
226- [1] = {
227- ['Type'] = 'Enum',
228- ['Formula'] = {
229- [1] = 1,
230- },
231- },
232- },
233- },
234- ['trapBobEnable'] = {
235- ['Type'] = 'integer',
236- ['Validator'] = {
237- [1] = {
238- ['Type'] = 'Enum',
239- ['Formula'] = {
240- [1] = 1,
241- [2] = 2,
242- },
243- },
244- },
245- },
246- },
247- },
248- Statements = {
249- ['Enabled'] = {
250- ['Input'] = '${ReqBody/trapReceiverEnable}',
251- ['Steps'] = {
252- [1] = {
253- ['Type'] = 'Switch',
254- ['Formula'] = {
255- [1] = {
256- ['Case'] = 1,
257- ['To'] = false,
258- },
259- [2] = {
260- ['Case'] = 2,
261- ['To'] = true,
262- },
263- },
264- },
265- },
266- },
267- ['BobEnabled'] = {
268- ['Input'] = '${ReqBody/trapBobEnable}',
269- ['Steps'] = {
270- [1] = {
271- ['Type'] = 'Switch',
272- ['Formula'] = {
273- [1] = {
274- ['Case'] = 1,
275- ['To'] = false,
276- },
277- [2] = {
278- ['Case'] = 2,
279- ['To'] = true,
280- },
281- },
282- },
283- },
284- },
285- },
286- ProcessingFlow = {
287- [1] = {
288- ['Type'] = 'Property',
289- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/${ReqBody/trapReceiverIndex}',
290- ['Interface'] = 'bmc.kepler.EventService.Subscriptions.Snmp.Nms',
291- ['Source'] = {
292- ['Enabled'] = '${Statements/Enabled()}',
293- ['BobEnabled'] = '${Statements/BobEnabled()}',
294- ['Address'] = '${ReqBody/trapReceiverAddress}',
295- ['Port'] = '${ReqBody/trapReceiverPort}',
296- },
297- },
298- [2] = {
299- ['Type'] = 'Method',
300- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/${ReqBody/trapReceiverIndex}',
301- ['Interface'] = 'bmc.kepler.EventService.Subscriptions.Snmp.Nms',
302- ['Name'] = 'SubmitTestEvent',
303- ['CallIf'] = '{"${ReqBody/trapTest}": "#WITH"}',
304- },
305- },
306- },
307- },
308- },
309- ['/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/:id'] = {
310- methods = {
311- get = {
312- RspBody = {
313- ['trapReceiverIndex'] = '${Statements/MemberId()}',
314- ['trapReceiverEnable'] = '${Statements/Enabled()}',
315- ['trapBobEnable'] = '${Statements/BobEnabled()}',
316- ['trapReceiverAddress'] = '${ProcessingFlow[1]/Destination/TrapServerAddress}',
317- ['trapReceiverPort'] = '${ProcessingFlow[1]/Destination/TrapServerPort}',
318- ['trapSendType'] = 1,
319- ['trapTest'] = 'write-only',
320- },
321- Statements = {
322- ['MemberId'] = {
323- ['Input'] = '${Uri/id}',
324- ['Steps'] = {
325- [1] = {
326- ['Type'] = 'Convert',
327- ['Formula'] = 'StringToNumber',
328- },
329- },
330- },
331- ['Enabled'] = {
332- ['Input'] = '${ProcessingFlow[1]/Destination/Enabled}',
333- ['Steps'] = {
334- [1] = {
335- ['Type'] = 'Switch',
336- ['Formula'] = {
337- [1] = {
338- ['Case'] = false,
339- ['To'] = 1,
340- },
341- [2] = {
342- ['Case'] = true,
343- ['To'] = 2,
344- },
345- },
346- },
347- },
348- },
349- ['BobEnabled'] = {
350- ['Input'] = '${ProcessingFlow[1]/Destination/BobEnabled}',
351- ['Steps'] = {
352- [1] = {
353- ['Type'] = 'Switch',
354- ['Formula'] = {
355- [1] = {
356- ['Case'] = false,
357- ['To'] = 1,
358- },
359- [2] = {
360- ['Case'] = true,
361- ['To'] = 2,
362- },
363- },
364- },
365- },
366- },
367- },
368- ProcessingFlow = {
369- [1] = {
370- ['Type'] = 'Property',
371- ['Path'] = '/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/${Uri/id}',
372- ['Interface'] = 'bmc.kepler.EventService.Subscriptions.Snmp.Nms',
373- ['Destination'] = {
374- ['Enabled'] = 'Enabled',
375- ['BobEnabled'] = 'BobEnabled',
376- ['Address'] = 'TrapServerAddress',
377- ['Port'] = 'TrapServerPort',
378- },
379- },
380- },
381- },
382- },
383- },
384-}
@@ -1,27 +0,0 @@
1--- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2--- openUBMC is licensed under Mulan PSL v2.
3--- You can use this software according to the terms and conditions of the Mulan PSL v2.
4--- You may obtain a copy of Mulan PSL v2 at:
5--- http://license.coscl.org.cn/MulanPSL2
6--- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7--- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8--- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9--- See the Mulan PSL v2 for more details.
10- 
11-Input.data = {
12- ['trap'] = {
13- ['trap.json'] = {
14- ['/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.1/trapEnable/Readwrite'] = {
15- ['get'] = true,
16- ['patch'] = true,
17- },
18- ['/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.50/trapInfoDescriptionTable/Readwrite'] = {
19- ['get'] = true,
20- ['patch'] = true,
21- },
22- ['/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/:id'] = {
23- ['get'] = true,
24- },
25- },
26- },
27-}
@@ -0,0 +1,227 @@
1+-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+-- openUBMC is licensed under Mulan PSL v2.
3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4+-- You may obtain a copy of Mulan PSL v2 at:
5+-- http://license.coscl.org.cn/MulanPSL2
6+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+-- See the Mulan PSL v2 for more details.
10+ 
11+-- 单元测试用 mock:模拟进程内路由映射器门面(routemapper.sync),
12+-- 数据格式与编译后的路由/端点描述一致(list_routes/describe 的返回结构)
13+ 
14+local mock = {}
15+ 
16+local TRAP_ENABLE_URI = '/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.1/trapEnable/Readwrite'
17+local TRAP_TABLE_URI = '/snmp/1.3.6.1.4.1.2011.2.235.1.1.4.50/trapInfoDescriptionTable/Readwrite'
18+local NMSES_URI = '/bmc/kepler/EventService/Subscriptions/Snmp/Nmses/:id'
19+ 
20+local ROUTES = {
21+ {
22+ Uri = TRAP_ENABLE_URI,
23+ Methods = { 'GET', 'PATCH' },
24+ IgnoreEtags = {},
25+ },
26+ {
27+ Uri = TRAP_TABLE_URI,
28+ Methods = { 'GET', 'PATCH' },
29+ IgnoreEtags = {},
30+ },
31+ {
32+ Uri = NMSES_URI,
33+ Methods = { 'GET' },
34+ IgnoreEtags = {},
35+ },
36+}
37+ 
38+local TRAP_TABLE_SEQUENCE = {
39+ { Name = 'trapReceiverIndex', Type = 'integer', Access = 'Readonly', Primary = true },
40+ { Name = 'trapReceiverEnable', Type = 'integer', Access = 'Readwrite', Primary = false },
41+ { Name = 'trapReceiverAddress', Type = 'string', Access = 'Readwrite', Primary = false },
42+ { Name = 'trapReceiverPort', Type = 'integer', Access = 'Readwrite', Primary = false },
43+ { Name = 'trapSendType', Type = 'integer', Access = 'Readwrite', Primary = false },
44+ { Name = 'trapTest', Type = 'string', Access = 'Setonly', Primary = false },
45+ { Name = 'trapBobEnable', Type = 'integer', Access = 'Readwrite', Primary = false },
46+}
47+ 
48+local function base_description(uri, method, extra)
49+ local description = {
50+ Source = 'mock_mapping_config',
51+ RoutePattern = uri,
52+ Method = method,
53+ UriParameters = {},
54+ Mapper = { Type = 'Ordinary' },
55+ TaskReferenceUri = {},
56+ Parameters = {},
57+ RequestErrors = {},
58+ ResponseFields = {},
59+ IgnoreEtags = {},
60+ Sequence = {},
61+ DataSources = {},
62+ }
63+ 
64+ for key, value in pairs(extra or {}) do
65+ description[key] = value
66+ end
67+ 
68+ return description
69+end
70+ 
71+local DESCRIPTIONS = {
72+ [TRAP_ENABLE_URI] = {
73+ GET = base_description(TRAP_ENABLE_URI, 'GET', {
74+ ResponseFields = { { Name = 'Enabled', Default = 1 } },
75+ }),
76+ PATCH = base_description(TRAP_ENABLE_URI, 'PATCH', {
77+ RequestSchema = {
78+ Type = 'object',
79+ Required = true,
80+ Properties = {
81+ Enabled = {
82+ Required = true,
83+ Type = 'integer',
84+ Validator = {},
85+ },
86+ },
87+ },
88+ }),
89+ },
90+ [TRAP_TABLE_URI] = {
91+ GET = base_description(TRAP_TABLE_URI, 'GET', {
92+ Sequence = TRAP_TABLE_SEQUENCE,
93+ ResponseFields = {
94+ { Name = 'trapReceiverIndex', Default = 1 },
95+ { Name = 'trapReceiverEnable', Default = 1 },
96+ { Name = 'trapReceiverAddress', Default = '' },
97+ { Name = 'trapReceiverPort', Default = 162 },
98+ { Name = 'trapSendType', Default = 1 },
99+ { Name = 'trapTest', Default = '' },
100+ { Name = 'trapBobEnable', Default = 1 },
101+ },
102+ }),
103+ PATCH = base_description(TRAP_TABLE_URI, 'PATCH', {
104+ RequestSchema = {
105+ Type = 'object',
106+ Required = true,
107+ Properties = {
108+ trapReceiverIndex = {
109+ Required = true,
110+ Type = 'integer',
111+ Validator = {},
112+ },
113+ trapReceiverEnable = {
114+ Required = false,
115+ Type = 'integer',
116+ Validator = {},
117+ },
118+ trapReceiverAddress = {
119+ Required = false,
120+ Type = 'string',
121+ Validator = {},
122+ },
123+ trapReceiverPort = {
124+ Required = false,
125+ Type = 'integer',
126+ Validator = {},
127+ },
128+ trapSendType = {
129+ Required = false,
130+ Type = 'integer',
131+ Validator = {},
132+ },
133+ trapTest = {
134+ Required = false,
135+ Type = 'string',
136+ Validator = {},
137+ },
138+ trapBobEnable = {
139+ Required = false,
140+ Type = 'integer',
141+ Validator = {},
142+ },
143+ },
144+ },
145+ }),
146+ },
147+ [NMSES_URI] = {
148+ GET = base_description(NMSES_URI, 'GET', {
149+ ResponseFields = { { Name = 'Nmses', Default = {} } },
150+ }),
151+ },
152+}
153+ 
154+-- 模拟 core.so 的异步接口:*_async 返回 mc.future,take_ready 取结果
155+local function ready_future(value)
156+ return {
157+ is_ready = function()
158+ return true
159+ end,
160+ value = value,
161+ }
162+end
163+ 
164+local mapper = {}
165+ 
166+function mapper:generation()
167+ return 1
168+end
169+ 
170+function mapper:list_routes()
171+ return ROUTES
172+end
173+ 
174+function mapper:describe(method, uri)
175+ local route = DESCRIPTIONS[uri]
176+ if route then
177+ return route[string.upper(method)]
178+ end
179+ 
180+ return nil
181+end
182+ 
183+function mapper:describe_async(method, uri)
184+ return ready_future(self:describe(method, uri))
185+end
186+ 
187+function mapper:execute(method, uri)
188+ if not (DESCRIPTIONS[uri] and DESCRIPTIONS[uri][string.upper(method)]) then
189+ return {
190+ status = 404,
191+ body = {},
192+ errors = { { Code = 'ResourceNotFound', Message = 'the mapped resource does not exist' } },
193+ headers = {},
194+ effects = {},
195+ }
196+ end
197+ 
198+ return {
199+ status = 200,
200+ body = {},
201+ errors = {},
202+ headers = {},
203+ effects = {},
204+ }
205+end
206+ 
207+function mapper:execute_async(method, uri)
208+ return ready_future(self:execute(method, uri))
209+end
210+ 
211+function mapper:take_ready(future)
212+ return future.value
213+end
214+ 
215+function mapper:reload()
216+ return 2
217+end
218+ 
219+function mock.load()
220+ return mapper
221+end
222+ 
223+function mock.new()
224+ return mapper
225+end
226+ 
227+return mock
@@ -10,13 +10,17 @@
10 10 
11local lu = require('luaunit')11local lu = require('luaunit')
12local json = require('cjson')12local json = require('cjson')
13-local c_interface = require('interface')
14 13 
15-local account = require('route_mapper.account')14+-- mock 进程内路由映射器的底层门面(routemapper.sync),
16-account.get_package_customer = function()15+-- 使 snmp_mapper 在单元测试环境无需真实路由映射器运行库;
17- return ''16+-- 必须在 require('interface') 之前注入,因为 interface 的 require 链会加载 snmp_mapper
17+local mock_routemapper = require('mock_routemapper')
18+package.preload['routemapper.sync'] = function()
19+ return mock_routemapper
18end20end
19 21 
22+local c_interface = require('interface')
23+ 
20local project_dir = os.getenv('PROJECT_DIR')24local project_dir = os.getenv('PROJECT_DIR')
21local MAPPER_CONF_PATH <const> = project_dir .. '/test/unit/data/mapping_config'25local MAPPER_CONF_PATH <const> = project_dir .. '/test/unit/data/mapping_config'
22local MDB_PATH_PREFIX <const> = '/bmc/kepler/'26local MDB_PATH_PREFIX <const> = '/bmc/kepler/'