Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions files/build_templates/docker_image_ctl.j2
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ start() {
{%- endif %}
docker create {{docker_image_run_opt}} \
-v /host/warmboot$DEV:/var/warmboot \
{#- gNMI/gNOI UDS for in-box clients. gnmi container owns it :rw. #}
{%- if docker_container_name in ("swss", "syncd", "pmon") %}
-v /var/run/gnmi:/var/run/gnmi:ro \
{%- endif %}
{%- if docker_container_name != "dhcp_server" %}
--net=$NET \
{%- endif %}
Expand Down
41 changes: 39 additions & 2 deletions src/sonic-py-common/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,34 @@
print(package + " version not match!", file=sys.stderr)
exit(1)

# The sonic_py_common.grpc.gnoi package depends on grpcio/protobuf, which
# no longer publish Python 2 wheels, and uses Python-3-only syntax
# (f-strings, type hints). Gate both the dependencies and the package
# entry on Python 3 so the legacy ENABLE_PY2_MODULES=y wheel build
# continues to succeed.
PY3 = sys.version_info[0] >= 3

extra_packages = []
extra_install_requires = []
if PY3:
extra_packages = [
'sonic_py_common.grpc',
'sonic_py_common.grpc.gnoi',
]
# protobuf 4.21 introduced the runtime-version check embedded in
# generated *_pb2.py stubs (see Protobuf Python Version header).
# Stubs in sonic_py_common/grpc/gnoi were generated with protoc-gen
# >= 4.25 and refuse to import under protobuf < 4.21, so pin the
# lower bound explicitly rather than silently importing on bullseye
# images that still ship the locally-built protobuf 3.21.12
# (rules/protobuf.mk). Modern SONiC base images (bookworm, trixie)
# already satisfy this; older ones get a clear ImportError instead
# of a low-level descriptor mismatch.
extra_install_requires = [
'grpcio',
'protobuf>=4.21',
]

setup(
name='sonic-py-common',
version='1.0',
Expand All @@ -36,10 +64,10 @@
url='https://github.com/Azure/SONiC',
maintainer='Joe LeVeque',
maintainer_email='jolevequ@microsoft.com',
install_requires=dependencies,
install_requires=dependencies + extra_install_requires,
packages=[
'sonic_py_common',
],
] + extra_packages,
setup_requires= [
'pytest-runner',
'wheel'
Expand All @@ -48,6 +76,15 @@
'pytest',
'mock==3.0.5' # For python 2. Version >=4.0.0 drops support for py2
],
extras_require={
# The bookworm/trixie wheel build pipeline does
# `pip install ".[testing]"` before running pytest
# (slave.mk). Provide the extra explicitly so the build
# doesn't fail with "has no extra 'testing'".
'testing': [
'pytest',
],
},
entry_points={
'console_scripts': [
'sonic-db-load = sonic_py_common.sonic_db_dump_load:sonic_db_dump_load',
Expand Down
10 changes: 10 additions & 0 deletions src/sonic-py-common/sonic_py_common/grpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
sonic_py_common.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
"""
19 changes: 19 additions & 0 deletions src/sonic-py-common/sonic_py_common/grpc/gnoi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
sonic_py_common.grpc.gnoi - gNOI Python framework for SONiC.

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

Usage:
from sonic_py_common.grpc.gnoi import GnoiClient
from sonic_py_common.grpc.gnoi import system_pb2

with GnoiClient("10.0.0.1:8080") as client:
request = system_pb2.RebootRequest(method=system_pb2.HALT)
client.system.Reboot(request, timeout=60)
"""

from sonic_py_common.grpc.gnoi.client import GnoiClient

__all__ = ['GnoiClient']
144 changes: 144 additions & 0 deletions src/sonic-py-common/sonic_py_common/grpc/gnoi/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""
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_py_common.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())

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

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

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

Loading
Loading