-
Notifications
You must be signed in to change notification settings - Fork 39
Added gRPC gnmi protocol to UTCP #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
d28dc58
018806c
0f2af7e
d28c0af
7ba8b3c
908cd40
74a11e2
03a4b9f
8443cda
0150a3b
6e2c671
9cea90f
7016987
718b668
ca252e5
45793cf
662d07d
3aed349
dca4d26
4a2aea4
21cbab7
700ec92
70ff230
9bbb819
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # UTCP gNMI Plugin | ||
|
|
||
| This plugin adds a gNMI (gRPC) communication protocol compatible with UTCP 1.0. It follows UTCP’s plugin architecture: a CallTemplate and serializer, a CommunicationProtocol for discovery and execution, and registration via the `utcp.plugins` entry point. | ||
|
|
||
| ## Installation | ||
|
|
||
| - Ensure you have Python 3.10+ | ||
| - Dependencies: `utcp`, `grpcio`, `protobuf`, `pydantic`, `aiohttp` | ||
| - Install in your environment (example if published): | ||
|
|
||
| ``` | ||
| pip install utcp-gnmi | ||
| ``` | ||
|
|
||
| ## Registration | ||
|
|
||
| Register the plugin into UTCP’s registries: | ||
|
|
||
| ``` | ||
| from utcp_gnmi import register | ||
| register() | ||
| ``` | ||
|
|
||
| This registers: | ||
| - Protocol: `gnmi` | ||
| - Call template serializer: `gnmi` | ||
|
|
||
| ## Configuration (UTCP 1.0) | ||
|
|
||
| Use `UtcpClientConfig.manual_call_templates` to declare gNMI providers and tools. | ||
|
|
||
| Example: | ||
|
|
||
| ``` | ||
| { | ||
| "manual_call_templates": [ | ||
| { | ||
| "name": "routerA", | ||
| "call_template_type": "gnmi", | ||
| "target": "localhost:50051", | ||
| "use_tls": false, | ||
| "metadata": {"authorization": "Bearer ${API_TOKEN}"}, | ||
| "metadata_fields": ["tenant-id"], | ||
| "operation": "get", | ||
| "stub_module": "gnmi_pb2_grpc", | ||
| "message_module": "gnmi_pb2" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| Fields: | ||
| - `call_template_type`: must be `gnmi` | ||
| - `target`: gRPC host:port | ||
| - `use_tls`: boolean; TLS required unless localhost/127.0.0.1 | ||
| - `metadata`: static key/value pairs added to gRPC metadata | ||
| - `metadata_fields`: dynamic keys populated from tool args | ||
| - `operation`: one of `capabilities`, `get`, `set`, `subscribe` | ||
| - `stub_module`/`message_module`: import paths to generated Python stubs | ||
|
|
||
| ## Security | ||
|
|
||
| - Enforces TLS (`grpc.aio.secure_channel`) unless `target` is `localhost` or `127.0.0.1` | ||
| - Do not use insecure channels over public networks | ||
| - Prefer mTLS for production environments (future enhancement adds cert fields) | ||
|
|
||
| ## Authentication | ||
|
|
||
| Supported via UTCP `Auth` model: | ||
| - API Key: injects into `authorization` (or custom) metadata | ||
| - Basic: `authorization: Basic <base64(user:pass)>` | ||
| - OAuth2: client credentials; token fetched via `aiohttp` and cached | ||
|
|
||
| ## Operations | ||
|
|
||
| - `capabilities`: unary `Capabilities` RPC | ||
| - `get`: unary `Get` RPC; maps `paths` list into `GetRequest.path` | ||
| - `set`: unary `Set` RPC; maps `updates` list into `SetRequest.update` | ||
| - `subscribe`: streaming `Subscribe` RPC; yields responses as dicts | ||
|
|
||
| ## Testing | ||
|
|
||
| Run tests: | ||
|
|
||
| ``` | ||
| python -m pytest plugins/communication_protocols/gnmi/tests/test_gnmi_plugin.py -q | ||
| ``` | ||
|
|
||
| The tests validate manual registration, tool presence (including `subscribe`), and serializer round-trip. | ||
|
|
||
| ## Notes | ||
|
|
||
| - Tool discovery registers canonical gNMI operations (`capabilities/get/set/subscribe`) | ||
| - Reflection-based discovery and mTLS configuration can be added in follow-up PRs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| [build-system] | ||
| requires = ["setuptools>=61.0"] | ||
| build-backend = "setuptools.build_meta" | ||
|
|
||
| [project] | ||
| name = "utcp-gnmi" | ||
| version = "1.0.0" | ||
| authors = [ | ||
| { name = "UTCP Contributors" }, | ||
| ] | ||
| description = "UTCP gNMI communication protocol plugin over gRPC" | ||
| readme = "README.md" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
| "pydantic>=2.0", | ||
| "protobuf>=4.21", | ||
| "grpcio>=1.60", | ||
| "utcp>=1.0", | ||
| "aiohttp>=3.8" | ||
| ] | ||
| license = "MPL-2.0" | ||
|
|
||
| [project.optional-dependencies] | ||
| dev = [ | ||
| "build", | ||
| "pytest", | ||
| "pytest-asyncio", | ||
| "pytest-cov", | ||
| ] | ||
|
|
||
| [project.entry-points."utcp.plugins"] | ||
| gnmi = "utcp_gnmi:register" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from utcp.plugins.discovery import register_communication_protocol, register_call_template | ||
| from utcp_gnmi.gnmi_communication_protocol import GnmiCommunicationProtocol | ||
| from utcp_gnmi.gnmi_call_template import GnmiCallTemplateSerializer | ||
|
|
||
| def register(): | ||
| register_communication_protocol("gnmi", GnmiCommunicationProtocol()) | ||
| register_call_template("gnmi", GnmiCallTemplateSerializer()) | ||
|
|
||
| __all__ = [ | ||
| "GnmiCommunicationProtocol", | ||
| "GnmiCallTemplateSerializer", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| from typing import Optional, Dict, List, Literal | ||
|
|
||
| from utcp.data.call_template import CallTemplate | ||
| from utcp.interfaces.serializer import Serializer | ||
| from utcp.exceptions import UtcpSerializerValidationError | ||
| import traceback | ||
|
|
||
| class GnmiCallTemplate(CallTemplate): | ||
| call_template_type: Literal["gnmi"] = "gnmi" | ||
| target: str | ||
| use_tls: bool = True | ||
| metadata: Optional[Dict[str, str]] = None | ||
| metadata_fields: Optional[List[str]] = None | ||
| operation: Literal["capabilities", "get", "set", "subscribe"] = "get" | ||
| stub_module: str = "gnmi_pb2_grpc" | ||
| message_module: str = "gnmi_pb2" | ||
|
Comment on lines
+8
to
+16
|
||
|
|
||
| class GnmiCallTemplateSerializer(Serializer[GnmiCallTemplate]): | ||
| def to_dict(self, obj: GnmiCallTemplate) -> dict: | ||
| return obj.model_dump() | ||
|
|
||
| def validate_dict(self, obj: dict) -> GnmiCallTemplate: | ||
| try: | ||
| return GnmiCallTemplate.model_validate(obj) | ||
| except Exception as e: | ||
| raise UtcpSerializerValidationError("Invalid GnmiCallTemplate: " + traceback.format_exc()) from e | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pyproject.toml references a README.md file that does not exist in the gnmi plugin directory. This will cause packaging issues. Either create a README.md file with appropriate documentation (similar to the GraphQL plugin's README) or remove the readme field from the project configuration.