Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions dockers/docker-sonic-mgmt/Dockerfile.j2
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{% from "dockers/dockerfile-macros.j2" import copy_files %}
{% set prefix = DEFAULT_CONTAINER_REGISTRY %}
FROM {{ prefix }}ubuntu:24.04

Expand Down Expand Up @@ -78,6 +79,7 @@ RUN uv pip install --no-cache \
dpugen \
future \
gitpython \
grpcio \
ipaddr \
ipython \
ixload \
Expand Down Expand Up @@ -190,6 +192,13 @@ RUN tmpdir=$(mktemp -d) \
&& cd / \
&& rm -rf "$tmpdir"

{% if docker_sonic_mgmt_whls.strip() %}
# Copy and install locally-built Python wheels (sonic-grpc, etc.).
# Installed via uv to match this image's /opt/venv (there is no pip in the venv).
{{ copy_files("python-wheels/", docker_sonic_mgmt_whls.split(' '), "/python-wheels/") }}
RUN uv pip install --no-cache {% for whl in docker_sonic_mgmt_whls.split(' ') %}/python-wheels/{{ whl }} {% endfor %}
{% endif %}

# Apply patches to ansible and ptf
COPY \
0001-Fix-getattr-AttributeError-in-multi-thread-scenario.patch \
Expand Down
1 change: 1 addition & 0 deletions rules/docker-sonic-mgmt.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
DOCKER_SONIC_MGMT = docker-sonic-mgmt.gz
$(DOCKER_SONIC_MGMT)_PATH = $(DOCKERS_PATH)/docker-sonic-mgmt
$(DOCKER_SONIC_MGMT)_DEPENDS += $(SONIC_DEVICE_DATA)
$(DOCKER_SONIC_MGMT)_PYTHON_WHEELS += $(SONIC_GRPC_PY3)
$(DOCKER_SONIC_MGMT)_FILES += $(GITHUB_GET)
SONIC_DOCKER_IMAGES += $(DOCKER_SONIC_MGMT)
SONIC_BOOKWORM_DOCKERS += $(DOCKER_SONIC_MGMT)
8 changes: 8 additions & 0 deletions rules/sonic-grpc.dep
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SPATH := $($(SONIC_GRPC_PY3)_SRC_PATH)
DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/sonic-grpc.mk rules/sonic-grpc.dep
DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST)
DEP_FILES += $(shell git ls-files $(SPATH))

$(SONIC_GRPC_PY3)_CACHE_MODE := GIT_CONTENT_SHA
$(SONIC_GRPC_PY3)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST)
$(SONIC_GRPC_PY3)_DEP_FILES := $(DEP_FILES)
6 changes: 6 additions & 0 deletions rules/sonic-grpc.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# sonic-grpc Python wheel (gNOI / gRPC client framework for SONiC)

SONIC_GRPC_PY3 = sonic_grpc-1.0-py3-none-any.whl
$(SONIC_GRPC_PY3)_SRC_PATH = $(SRC_PATH)/sonic-grpc
$(SONIC_GRPC_PY3)_PYTHON_VERSION = 3
SONIC_PYTHON_WHEELS += $(SONIC_GRPC_PY3)
29 changes: 29 additions & 0 deletions src/sonic-grpc/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from setuptools import setup

setup(
name='sonic-grpc',
version='1.0',
description='gRPC client frameworks for SONiC (gNOI, and future gNMI/gRIBI)',
license='Apache 2.0',
author='SONiC Team',
author_email='linuxnetdev@microsoft.com',
url='https://github.com/sonic-net/sonic-buildimage',
packages=[
'sonic_grpc',
'sonic_grpc.gnoi',
],
python_requires='>=3.9',
install_requires=[
'grpcio',
'protobuf>=4.21',
],
Comment on lines +16 to +19
extras_require={
'testing': ['pytest'],
},
classifiers=[
'Intended Audience :: Developers',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
],
keywords='sonic SONiC gnoi grpc',
)
10 changes: 10 additions & 0 deletions src/sonic-grpc/sonic_grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
sonic_grpc - gRPC framework for SONiC.

Sub-packages:
- gnoi: gNOI service stubs and client (System, Healthz, Cert, etc.)

Future:
- gnmi: gNMI service stubs and client
- gribi: gRIBI service stubs and client
"""
20 changes: 20 additions & 0 deletions src/sonic-grpc/sonic_grpc/gnoi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
sonic_grpc.gnoi - gNOI Python framework for SONiC.

Provides:
- Vendored gNOI proto stubs (System, File, types, common)
- GnoiClient: gRPC channel manager with service stub access

Usage:
from sonic_grpc.gnoi import GnoiClient
from sonic_grpc.gnoi import system_pb2, file_pb2

with GnoiClient("10.0.0.1:8080") as client:
request = system_pb2.RebootRequest(method=system_pb2.HALT)
client.system.Reboot(request, timeout=60)
client.file.Stat(file_pb2.StatRequest(path="/etc/hostname"))
"""

from sonic_grpc.gnoi.client import GnoiClient

__all__ = ['GnoiClient']
147 changes: 147 additions & 0 deletions src/sonic-grpc/sonic_grpc/gnoi/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
gNOI gRPC client for SONiC.

Manages a gRPC channel and provides access to gNOI service stubs.
All RPCs are accessed through service properties:

with GnoiClient("10.0.0.1:8080") as client:
client.system.Reboot(request, timeout=60)
client.system.RebootStatus(request, timeout=10)

For Unix-domain-socket targets (e.g. the local gNMI/gNOI server's
``/var/run/gnmi/gnmi.sock``), use the ``unix://`` scheme; no TLS is
needed for loopback IPC:

with GnoiClient("unix:///var/run/gnmi/gnmi.sock") as client:
client.system.Time(system_pb2.TimeRequest(), timeout=5)

For mTLS targets (e.g. the SONiC telemetry/gNOI server), pass a
grpc.ChannelCredentials built from the relevant cert files:

import grpc
creds = grpc.ssl_channel_credentials(
root_certificates=open("/etc/sonic/telemetry/streamingtelemetryserver.cer", "rb").read(),
private_key=open("/etc/sonic/telemetry/dsmsroot.key", "rb").read(),
certificate_chain=open("/etc/sonic/telemetry/dsmsroot.cer", "rb").read(),
)
options = (("grpc.ssl_target_name_override", "ndastreamingservertest"),)
with GnoiClient("127.0.0.1:50052", credentials=creds, options=options) as client:
client.system.Time(system_pb2.TimeRequest(), timeout=5)

The client is service-agnostic — new gNOI services (Healthz, Cert,
File, OS, etc.) can be added as properties without modifying existing
code.
"""

import grpc
from sonic_grpc.gnoi import system_pb2_grpc


class GnoiClient:
"""gNOI gRPC client for SONiC components.

Manages a single gRPC channel and exposes gNOI service stubs as
properties. Use as a context manager for automatic cleanup.

Args:
target: gRPC target address, e.g. "10.0.0.1:8080".
options: Optional list of gRPC channel options, e.g.
``(("grpc.ssl_target_name_override", "server-cn"),)``.
credentials: Optional ``grpc.ChannelCredentials``. When ``None``
(default), an insecure channel is opened — suitable for
localhost loopback or test fakes. When supplied, a secure
channel is opened; build credentials with
``grpc.ssl_channel_credentials(...)``.

Example (insecure, for FakeGnoiServer or a localhost helper):
with GnoiClient("localhost:50051") as client:
client.system.Reboot(req, timeout=60)

Example (Unix domain socket, no TLS):
with GnoiClient("unix:///var/run/gnmi/gnmi.sock") as client:
client.system.Time(system_pb2.TimeRequest(), timeout=5)

Example (mTLS):
creds = grpc.ssl_channel_credentials(
root_certificates=server_ca_pem,
private_key=client_key_pem,
certificate_chain=client_cert_pem,
)
with GnoiClient("dut:50052", credentials=creds) as client:
client.system.Time(system_pb2.TimeRequest(), timeout=5)
"""

def __init__(self, target, options=None, credentials=None):
self._target = target
self._options = options
self._credentials = credentials
self._channel = None

def __enter__(self):
if self._credentials is None:
self._channel = grpc.insecure_channel(
self._target, options=self._options
)
else:
self._channel = grpc.secure_channel(
self._target, self._credentials, options=self._options
)
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False

def close(self):
"""Close the gRPC channel."""
if self._channel is not None:
self._channel.close()
self._channel = None

@property
def channel(self):
"""The underlying gRPC channel for constructing custom stubs."""
return self._channel

def _require_channel(self):
if self._channel is None:
raise RuntimeError(
"GnoiClient channel is not open. Use it as a context manager "
"(`with GnoiClient(target) as client:`) or call __enter__() "
"before accessing service stubs."
)
return self._channel

# ---- gNOI service stubs ----

@property
def system(self):
"""gNOI System service stub (gnoi.system.System).

Provides: Reboot, RebootStatus, CancelReboot, Time, Ping,
Traceroute, SwitchControlProcessor, etc.
"""
return system_pb2_grpc.SystemStub(self._require_channel())

@property
def file(self):
"""gNOI File service stub (gnoi.file.File).

Provides: Get, Put, Stat, Remove, TransferToRemote.
"""
from sonic_grpc.gnoi import file_pb2_grpc
return file_pb2_grpc.FileStub(self._require_channel())

# Future services can be added here:
#
# @property
# def healthz(self):
# """gNOI Healthz service stub."""
# from sonic_grpc.gnoi import healthz_pb2_grpc
# return healthz_pb2_grpc.HealthzStub(self._channel)
#
# @property
# def cert(self):
# """gNOI CertificateManagement service stub."""
# from sonic_grpc.gnoi import cert_pb2_grpc
# return cert_pb2_grpc.CertificateManagementStub(self._channel)
30 changes: 30 additions & 0 deletions src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/sonic-grpc/sonic_grpc/gnoi/common_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

63 changes: 63 additions & 0 deletions src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading