Explorar o código

修改bacnet监听

Lu Xianghui hai 1 mes
pai
achega
4d17dcf8dd

+ 52 - 6
app/services/bacnet_service.py

@@ -45,6 +45,11 @@ except Exception:
     pass
 
 
+class BacnetCommunicationError(RuntimeError):
+    def __init__(self, message: str) -> None:
+        super().__init__(message)
+
+
 def device_payload(request: BacnetPointReadRequest | BacnetPointSearchRequest) -> dict[str, Any]:
     return {
         "device_type": "BACnet/IP",
@@ -107,7 +112,38 @@ async def read_property_or_none(bacnet: Any, address: str, object_type: str, obj
 
 
 async def read_present_value(bacnet: Any, address: str, point: BacnetPointSpec) -> Any:
-    return json_value(await bacnet.read(property_read_args(address, point.object_type, point.object_id, "presentValue")))
+    try:
+        value = await bacnet.read(property_read_args(address, point.object_type, point.object_id, "presentValue"))
+    except Exception as exc:
+        raise BacnetCommunicationError(
+            f"no response from BACnet device {address} while reading "
+            f"{point.object_type} {point.object_id} presentValue"
+        ) from exc
+    return json_value(value)
+
+
+def ignore_cancelled_bacnet_broadcast_endpoint(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
+    message = str(context.get("message", ""))
+    exception = context.get("exception")
+    if isinstance(exception, asyncio.CancelledError) and "set_broadcast_transport_protocol" in message:
+        return
+    loop.default_exception_handler(context)
+
+
+def run_bacnet(coro: Any) -> Any:
+    loop = asyncio.new_event_loop()
+    loop.set_exception_handler(ignore_cancelled_bacnet_broadcast_endpoint)
+    try:
+        asyncio.set_event_loop(loop)
+        return loop.run_until_complete(coro)
+    finally:
+        pending = asyncio.all_tasks(loop)
+        for task in pending:
+            task.cancel()
+        if pending:
+            loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
+        loop.close()
+        asyncio.set_event_loop(None)
 
 
 def split_object_identifier(value: Any) -> tuple[str, int] | None:
@@ -163,9 +199,15 @@ async def search_points_async(request: BacnetPointSearchRequest) -> list[dict[st
     with warnings.catch_warnings():
         warnings.filterwarnings("ignore", message=BAC0_REGISTERED_WARNING)
         async with BAC0.start(ip=local_ip, port=local_port, ping=False) as bacnet:
-            object_list = await bacnet.read(
-                property_read_args(address, "Device", request.bacnet_device_id, "objectList")
-            )
+            try:
+                object_list = await bacnet.read(
+                    property_read_args(address, "Device", request.bacnet_device_id, "objectList")
+                )
+            except Exception as exc:
+                raise BacnetCommunicationError(
+                    f"no response from BACnet device {address} while reading "
+                    f"Device {request.bacnet_device_id} objectList"
+                ) from exc
             for item in object_list or []:
                 parsed = split_object_identifier(item)
                 if parsed is None:
@@ -188,7 +230,9 @@ async def search_points_async(request: BacnetPointSearchRequest) -> list[dict[st
 def read_points(request: BacnetPointReadRequest) -> dict[str, Any]:
     try:
         with _BACNET_LOCK:
-            points = asyncio.run(read_points_async(request))
+            points = run_bacnet(read_points_async(request))
+    except BacnetCommunicationError as exc:
+        return response_payload(1, str(exc), {"device": device_payload(request), "points": []})
     except Exception as exc:
         return response_payload(1, str(exc), {"device": device_payload(request), "points": []})
     return response_payload(0, "success", {"device": device_payload(request), "points": points})
@@ -197,7 +241,9 @@ def read_points(request: BacnetPointReadRequest) -> dict[str, Any]:
 def search_points(request: BacnetPointSearchRequest) -> dict[str, Any]:
     try:
         with _BACNET_LOCK:
-            points = asyncio.run(search_points_async(request))
+            points = run_bacnet(search_points_async(request))
+    except BacnetCommunicationError as exc:
+        return response_payload(1, str(exc), {"device": device_payload(request), "points": []})
     except Exception as exc:
         return response_payload(1, str(exc), {"device": device_payload(request), "points": []})
     return response_payload(0, "success", {"device": device_payload(request), "points": points})

+ 107 - 23
docs/bacnet-http-api.md

@@ -1,10 +1,22 @@
 # BACnet HTTP 接口说明
 
-本服务通过 HTTP 接口连接 BACnet/IP 设备。BACnet 功能使用 `BAC0` 实现,当前只提供点位读取和点位搜索。
+## 目标
 
-## 通用响应
+本服务通过 HTTP 接口连接 BACnet/IP 设备。BACnet 功能使用 `BAC0` 实现,当前提供点位读取和点位搜索。
 
-所有接口成功或失败均返回 HTTP `200`,业务结果由响应体 `code` 判断。
+当前仅支持 `BACnet/IP`。
+
+## 接口列表
+
+- `POST /api/dc-gateway/bacnet/read_points`:按点位对象读取,固定返回 `present-value`。
+- `POST /api/dc-gateway/bacnet/search_points`:读取设备 `object-list`,搜索常见 BACnet 点位对象并返回基础信息和值。
+- `GET /api/dc-gateway/health`:健康检查。
+
+## 通用返回约定
+
+接口业务结果统一使用 HTTP `200` 返回,是否成功由 JSON 中的 `code` 判断。
+
+成功:
 
 ```json
 {
@@ -14,21 +26,37 @@
 }
 ```
 
+失败:
+
+```json
+{
+  "code": 1,
+  "msg": "BACnet communication error",
+  "data": {
+    "points": []
+  }
+}
+```
+
+字段说明:
+
 | 字段 | 类型 | 说明 |
 |---|---|---|
 | `code` | integer | `0` 表示成功,`1` 表示失败。 |
 | `msg` | string | 成功时为 `success`,失败时为错误信息。 |
 | `data` | object | 接口数据。 |
 
-## 设备字段
+`data` 固定为对象。两个 BACnet 接口都会在 `data.device` 返回设备信息,点位列表返回在 `data.points`。
 
-以下字段用于 `/bacnet/read_points` 和 `/bacnet/search_points`。
+## 设备参数校验
 
-| 字段 | 类型 | 必填 | 默认值 | 说明 |
-|---|---|---|---|---|
-| `ip` | string | 是 | 无 | BACnet/IP 设备地址。 |
-| `bacnet_device_id` | integer | 是 | 无 | BACnet 设备对象实例号,范围 `0..4194303`。 |
-| `port` | integer | 否 | `47808` | BACnet/IP UDP 端口。 |
+以下字段用于 `/bacnet/read_points` 和 `/bacnet/search_points` 请求体顶层。
+
+| 字段 | 是否必传 | 默认值 | 校验 | 说明 |
+|---|---:|---|---|---|
+| `ip` | 是 | 无 | 必须是合法 IP 地址 | BACnet/IP 设备地址。 |
+| `bacnet_device_id` | 是 | 无 | `0..4194303` | BACnet 设备对象实例号。 |
+| `port` | 否 | `47808` | `1..65535` | BACnet/IP UDP 端口。 |
 
 响应中的设备信息固定包含:
 
@@ -59,6 +87,8 @@
 | `MultiStateOutput` | 多状态输出 |
 | `MultiStateValue` | 多状态值 |
 
+`/bacnet/search_points` 只返回上表中的常见点位对象类型。`/bacnet/read_points` 可按对象类型读取 `present-value`,但目标对象必须支持该属性。
+
 ## 接口一:读取点位
 
 ### URL
@@ -85,11 +115,13 @@ POST /api/dc-gateway/bacnet/read_points
 }
 ```
 
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `points` | object[] | 是 | 至少 1 个点位。 |
-| `points[].object_type` | string | 是 | BACnet 对象类型。 |
-| `points[].object_id` | integer | 是 | BACnet 对象实例号,范围 `0..4194303`。 |
+字段说明:
+
+| 字段 | 类型 | 必填 | 校验 | 说明 |
+|---|---|---:|---|---|
+| `points` | object[] | 是 | 至少 1 个点位 | BACnet 点位对象列表。 |
+| `points[].object_type` | string | 是 | 合法 BACnet 对象类型写法 | BACnet 对象类型。 |
+| `points[].object_id` | integer | 是 | `0..4194303` | BACnet 对象实例号。 |
 
 ### 成功返回
 
@@ -115,6 +147,38 @@ POST /api/dc-gateway/bacnet/read_points
 }
 ```
 
+### 失败返回
+
+设备通信失败:
+
+```json
+{
+  "code": 1,
+  "msg": "No response from BACnet device",
+  "data": {
+    "device": {
+      "device_type": "BACnet/IP",
+      "ip": "192.168.75.240",
+      "port": 47808,
+      "bacnet_device_id": 12345
+    },
+    "points": []
+  }
+}
+```
+
+请求字段校验失败:
+
+```json
+{
+  "code": 1,
+  "msg": "points: List should have at least 1 item after validation, not 0",
+  "data": {
+    "points": []
+  }
+}
+```
+
 ## 接口二:搜索点位
 
 ### URL
@@ -171,6 +235,34 @@ POST /api/dc-gateway/bacnet/search_points
 
 单个点位的 `description` 或 `present_value` 读取失败时,该字段返回 `null`,不会中断整个搜索。设备连接或 `object-list` 读取失败时,接口返回 `code=1`。
 
+### 失败返回
+
+```json
+{
+  "code": 1,
+  "msg": "No response from BACnet device",
+  "data": {
+    "device": {
+      "device_type": "BACnet/IP",
+      "ip": "192.168.75.240",
+      "port": 47808,
+      "bacnet_device_id": 12345
+    },
+    "points": []
+  }
+}
+```
+
+## 本地 BACnet 客户端绑定
+
+网关会为每次 BACnet 请求启动本地 BACnet 客户端,并自动推断到目标设备的本机地址。可通过环境变量覆盖:
+
+| 字段 | 默认值 | 说明 |
+|---|---|---|
+| `BACNET_LOCAL_IP` | 自动按到目标设备的路由推断 | 本地 BACnet 客户端绑定 IP。 |
+| `BACNET_LOCAL_MASK` | `24` | 本地 BACnet 客户端网络掩码位数。 |
+| `BACNET_LOCAL_PORT` | 自动选择空闲 UDP 端口 | 本地 BACnet 客户端绑定 UDP 端口。 |
+
 ## 测试默认设备
 
 BACnet 集成测试默认设备参数:
@@ -180,11 +272,3 @@ BACnet 集成测试默认设备参数:
 | `BACNET_DEVICE_IP` | `192.168.75.240` |
 | `BACNET_DEVICE_ID` | `12345` |
 | `BACNET_PORT` | `47808` |
-
-本地 BACnet 客户端绑定地址默认自动推断,可通过以下环境变量覆盖:
-
-| 字段 | 默认值 |
-|---|---|
-| `BACNET_LOCAL_IP` | 自动按到目标设备的路由推断 |
-| `BACNET_LOCAL_MASK` | `24` |
-| `BACNET_LOCAL_PORT` | 自动选择空闲 UDP 端口 |

+ 41 - 1
docs/localhost-data-colllector-gateway.md

@@ -4,7 +4,7 @@
 
 本文档用于说明 Data Collector Gateway 的 HTTP 接口,目标是让 AI 或自动化程序能够根据本文档直接构造请求、理解每个接口的用途、理解每个字段的含义,并正确解析返回结果。
 
-当前接口支持通过 HTTP 调用 Modbus TCP 和 Siemens S7 TCP 设备。S7 详细请求和响应约定见 [s7-http-api.md](s7-http-api.md)。
+当前接口支持通过 HTTP 调用 Modbus TCP、Siemens S7 TCP 和 BACnet/IP 设备。S7 详细请求和响应约定见 [s7-http-api.md](s7-http-api.md),BACnet 详细请求和响应约定见 [bacnet-http-api.md](bacnet-http-api.md)。
 
 ## 服务器请求地址
 
@@ -83,6 +83,8 @@ AI 调用接口时应按以下规则判断结果:
 | 原始 S7 读取 | `POST` | `/api/dc-gateway/s7/read` | 读取 S7 原始字节,只返回语义化 `communication`。 |
 | S7 点位读取并转换 | `POST` | `/api/dc-gateway/s7/read_points` | 按点位定义读取 S7 数据,并按数据类型转换为业务值。 |
 | S7 连接扫描 | `POST` | `/api/dc-gateway/s7/connect_scan` | 传 IP,可选 `device_type`,扫描可用的 `rock`、`slot`、`tsap_conn_type` 组合。 |
+| BACnet 点位读取 | `POST` | `/api/dc-gateway/bacnet/read_points` | 按 BACnet 对象读取 `present-value`。 |
+| BACnet 点位搜索 | `POST` | `/api/dc-gateway/bacnet/search_points` | 读取设备 `object-list`,返回常见点位对象信息和值。 |
 
 ## S7 接口快速说明
 
@@ -146,6 +148,44 @@ POST /api/dc-gateway/s7/connect_scan
 }
 ```
 
+## BACnet 接口快速说明
+
+BACnet 当前支持 BACnet/IP。`/bacnet/read_points` 固定读取每个对象的 `present-value`;`/bacnet/search_points` 读取设备对象的 `object-list`,并过滤 `AnalogInput`、`AnalogOutput`、`AnalogValue`、`BinaryInput`、`BinaryOutput`、`BinaryValue`、`MultiStateInput`、`MultiStateOutput`、`MultiStateValue` 等常见点位对象。
+
+BACnet 通用设备字段:
+
+| 字段 | 类型 | 必填 | 默认值 | 允许值或范围 | 含义 |
+|---|---|---:|---|---|---|
+| `ip` | string | 是 | 无 | 合法 IPv4 或 IPv6 地址 | BACnet/IP 设备地址。 |
+| `bacnet_device_id` | integer | 是 | 无 | `0..4194303` | BACnet 设备对象实例号。 |
+| `port` | integer | 否 | `47808` | `1..65535` | BACnet/IP UDP 端口。 |
+
+BACnet 点位读取请求示例:
+
+```json
+{
+  "ip": "192.168.75.240",
+  "bacnet_device_id": 12345,
+  "port": 47808,
+  "points": [
+    {
+      "object_type": "AnalogInput",
+      "object_id": 1
+    }
+  ]
+}
+```
+
+BACnet 点位搜索请求示例:
+
+```json
+{
+  "ip": "192.168.75.240",
+  "bacnet_device_id": 12345,
+  "port": 47808
+}
+```
+
 ## 通用设备字段
 
 以下字段用于描述要连接的 Modbus TCP 设备,出现在 `/modbus/read` 和 `/modbus/read_points` 请求体的顶层。

+ 63 - 0
intergration/bacnet/test_bacnet_service.py

@@ -0,0 +1,63 @@
+import asyncio
+import unittest
+
+from app.schemas.bacnet import BacnetPointReadRequest
+from app.services import bacnet_service
+
+
+class FailingBacnetContext:
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc, tb):
+        return False
+
+    async def read(self, _args):
+        raise RuntimeError("timeout")
+
+
+class BacnetServiceTest(unittest.TestCase):
+    def test_read_points_returns_clear_no_response_error(self):
+        original_start = bacnet_service.BAC0.start
+        original_local_ip = bacnet_service.local_bacnet_ip
+        original_local_port = bacnet_service.get_free_udp_port
+        bacnet_service.BAC0.start = lambda **_kwargs: FailingBacnetContext()
+        bacnet_service.local_bacnet_ip = lambda _ip, _port: "127.0.0.1/24"
+        bacnet_service.get_free_udp_port = lambda: 47809
+        self.addCleanup(setattr, bacnet_service.BAC0, "start", original_start)
+        self.addCleanup(setattr, bacnet_service, "local_bacnet_ip", original_local_ip)
+        self.addCleanup(setattr, bacnet_service, "get_free_udp_port", original_local_port)
+
+        response = bacnet_service.read_points(
+            BacnetPointReadRequest(
+                ip="192.168.1.10",
+                bacnet_device_id=12345,
+                points=[{"object_type": "analogInput", "object_id": 1}],
+            )
+        )
+
+        self.assertEqual(1, response["code"])
+        self.assertIn("no response from BACnet device 192.168.1.10:47808", response["msg"])
+        self.assertEqual([], response["data"]["points"])
+
+    def test_ignores_cancelled_broadcast_endpoint_callback(self):
+        class Loop:
+            default_handler_called = False
+
+            def default_exception_handler(self, _context):
+                self.default_handler_called = True
+
+        loop = Loop()
+        bacnet_service.ignore_cancelled_bacnet_broadcast_endpoint(
+            loop,
+            {
+                "message": "Exception in callback IPv4DatagramServer.set_broadcast_transport_protocol()",
+                "exception": asyncio.CancelledError(),
+            },
+        )
+
+        self.assertFalse(loop.default_handler_called)
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 181 - 0
test/BACnet-Client.txt

@@ -0,0 +1,181 @@
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: data-collector
+  namespace: proj-demo
+  labels:
+    app: data-collector
+  annotations:
+    deployment.kubernetes.io/revision: '18'
+    kubesphere.io/creator: admin
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: data-collector
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: data-collector
+        pod-template-hash: 7688557f74
+      annotations:
+        kubectl.kubernetes.io/restartedAt: '2024-09-14T14:59:16+08:00'
+        kubesphere.io/creator: admin
+        kubesphere.io/restartedAt: '2024-11-15T02:50:54.223Z'
+    spec:
+      volumes:
+        - name: auth-secret
+          hostPath:
+            path: /mnt/projects/proj_demo/auth_backend/secret
+            type: ''
+        - name: collector-data
+          hostPath:
+            path: /mnt/projects/proj_demo/data-collector/data/
+            type: ''
+        - name: data-collector-database
+          configMap:
+            name: data-collector-center
+            items:
+              - key: data-collector-database
+                path: database
+            defaultMode: 420
+        - name: data-collector-redis
+          configMap:
+            name: data-collector-center
+            items:
+              - key: data-collector-redis
+                path: redis
+            defaultMode: 420
+        - name: data-collector-logger
+          configMap:
+            name: data-collector-center
+            items:
+              - key: data-collector-logger
+                path: logger
+            defaultMode: 420
+        - name: data-collector-utils
+          configMap:
+            name: data-collector-center
+            items:
+              - key: data-collector-utils
+                path: utils
+            defaultMode: 420
+      containers:
+        - name: container-1czcgr
+          image: >-
+            dev.data-turing.cn:14443/dieteng/data-collector-master:20260323140950
+          ports:
+            - name: http-8085
+              containerPort: 8085
+              protocol: TCP
+            - name: udp-30732
+              containerPort: 30732
+              protocol: UDP
+          resources: {}
+          volumeMounts:
+            - name: auth-secret
+              mountPath: /app/data/auth_backend/
+            - name: collector-data
+              mountPath: /app/data/
+            - name: data-collector-database
+              mountPath: /app/config/database
+              subPath: database
+            - name: data-collector-redis
+              mountPath: /app/config/redis
+              subPath: redis
+            - name: data-collector-logger
+              mountPath: /app/config/logger
+              subPath: logger
+            - name: data-collector-utils
+              mountPath: /app/config/utils
+              subPath: utils
+          livenessProbe:
+            httpGet:
+              path: /internal/health
+              port: 8085
+              scheme: HTTP
+            timeoutSeconds: 3
+            periodSeconds: 15
+            successThreshold: 1
+            failureThreshold: 8
+          terminationMessagePath: /dev/termination-log
+          terminationMessagePolicy: File
+          imagePullPolicy: IfNotPresent
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      dnsPolicy: ClusterFirst
+      serviceAccountName: default
+      serviceAccount: default
+      securityContext: {}
+      imagePullSecrets:
+        - name: registry-harbor
+      schedulerName: default-scheduler
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 25%
+      maxSurge: 25%
+  revisionHistoryLimit: 10
+  progressDeadlineSeconds: 600
+
+
+
+
+
+
+kind: Service
+apiVersion: v1
+metadata:
+  name: data-collector-udp
+  namespace: proj-demo
+  labels:
+    app: data-collector-udp
+  annotations:
+    kubesphere.io/creator: admin
+spec:
+  ports:
+    - name: udp-47808
+      protocol: UDP
+      port: 30732
+      targetPort: 30732
+      nodePort: 30732
+  selector:
+    app: data-collector
+  clusterIP: 10.233.19.171
+  clusterIPs:
+    - 10.233.19.171
+  type: NodePort
+  sessionAffinity: None
+  externalTrafficPolicy: Cluster
+  ipFamilies:
+    - IPv4
+  ipFamilyPolicy: SingleStack
+  internalTrafficPolicy: Cluster
+  
+kind: Service
+apiVersion: v1
+metadata:
+  name: data-collector-svc
+  namespace: proj-demo
+  labels:
+    app: data-collector-svc
+  annotations:
+    kubesphere.io/creator: admin
+spec:
+  ports:
+    - name: http-8085
+      protocol: TCP
+      port: 8085
+      targetPort: 8085
+  selector:
+    app: data-collector
+  clusterIP: 10.233.2.75
+  clusterIPs:
+    - 10.233.2.75
+  type: ClusterIP
+  sessionAffinity: None
+  ipFamilies:
+    - IPv4
+  ipFamilyPolicy: SingleStack
+  internalTrafficPolicy: Cluster

+ 111 - 0
test/gateway.txt

@@ -0,0 +1,111 @@
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: data-collector-gateway
+  namespace: proj-data-collector
+  labels:
+    app: data-collector-gateway
+  annotations:
+    deployment.kubernetes.io/revision: '6'
+    kubesphere.io/creator: admin
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: data-collector-gateway
+  template:
+    metadata:
+      creationTimestamp: null
+      labels:
+        app: data-collector-gateway
+      annotations:
+        kubesphere.io/creator: admin
+    spec:
+      containers:
+        - name: container-crarci
+          image: >-
+            dev.data-turing.cn:14443/dieteng/data-collector-gateway:20260624082520
+          ports:
+            - name: http-8000
+              containerPort: 8000
+              protocol: TCP
+            - name: udp-47808
+              containerPort: 47808
+              protocol: UDP
+          resources: {}
+          terminationMessagePath: /dev/termination-log
+          terminationMessagePolicy: File
+          imagePullPolicy: IfNotPresent
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 30
+      dnsPolicy: ClusterFirst
+      serviceAccountName: default
+      serviceAccount: default
+      securityContext: {}
+      imagePullSecrets:
+        - name: registry-harbor
+      schedulerName: default-scheduler
+  strategy:
+    type: RollingUpdate
+    rollingUpdate:
+      maxUnavailable: 25%
+      maxSurge: 25%
+  revisionHistoryLimit: 10
+  progressDeadlineSeconds: 600
+
+kind: Service
+apiVersion: v1
+metadata:
+  name: data-collector-gateway-svc
+  namespace: proj-data-collector
+  labels:
+    app: data-collector-gateway-svc
+  annotations:
+    kubesphere.io/creator: admin
+spec:
+  ports:
+    - name: http-8000
+      protocol: TCP
+      port: 8000
+      targetPort: 8000
+      nodePort: 49328
+  selector:
+    app: data-collector-gateway
+  clusterIP: 10.233.53.204
+  clusterIPs:
+    - 10.233.53.204
+  type: NodePort
+  sessionAffinity: None
+  externalTrafficPolicy: Cluster
+  ipFamilies:
+    - IPv4
+  ipFamilyPolicy: SingleStack
+  internalTrafficPolicy: Cluster
+
+
+kind: Service
+apiVersion: v1
+metadata:
+  name: data-collector-gateway-udp-svc
+  namespace: proj-data-collector
+  annotations:
+    kubesphere.io/creator: admin
+spec:
+  ports:
+    - name: udp-47808
+      protocol: UDP
+      port: 47808
+      targetPort: 47808
+      nodePort: 30730
+  selector:
+    app: data-collector-gateway
+  clusterIP: 10.233.36.239
+  clusterIPs:
+    - 10.233.36.239
+  type: NodePort
+  sessionAffinity: None
+  externalTrafficPolicy: Cluster
+  ipFamilies:
+    - IPv4
+  ipFamilyPolicy: SingleStack
+  internalTrafficPolicy: Cluster