From cbf7baad8d32e36b69b4deb2a307322fcbe8523e Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Thu, 9 Jul 2026 17:00:11 -0500 Subject: [PATCH 1/4] [sonic-grpc]: Add gNOI Python gRPC client framework wheel Add a new standalone `sonic-grpc` Python distribution providing a native, in-process gNOI gRPC client for SONiC, and wire it into docker-sonic-mgmt. sonic_grpc.gnoi provides: - GnoiClient: a context-managed gRPC channel wrapper exposing gNOI service stubs as properties (client.system.*, client.file.*), over insecure TCP, unix:// UDS, or mTLS. Service-agnostic; new services plug in as properties. - Vendored flat gNOI proto stubs (System, File, types, common) regenerated against protobuf 4.x/5.x. No stub generation or gnoi.proto files needed at install time. - FakeGnoiServer test doubles for offline unit testing of gNOI-driven code. Packaged as its own minimal-dependency wheel (grpcio + protobuf only) so lightweight consumers - notably the docker-sonic-mgmt test image - can install it without pulling in a heavier package or its dependency tree. It installs anywhere, including: pip install "git+https://github.com/sonic-net/sonic-buildimage#subdirectory=src/sonic-grpc" Wired into docker-sonic-mgmt via rules/sonic-grpc.mk (SONIC_GRPC_PY3) and a uv-based install block in the Dockerfile (the image's /opt/venv has no pip, so the generic pip3 install_python_wheels macro is not used). grpcio is added to the image's package list as an explicit dependency. The gNOI client code originates from the framework in #27760; this PR delivers it as a reusable standalone wheel that #27760 and the sonic-mgmt gNOI test suite can both consume. Signed-off-by: Dawei Huang --- dockers/docker-sonic-mgmt/Dockerfile.j2 | 8 + rules/docker-sonic-mgmt.mk | 1 + rules/sonic-grpc.mk | 6 + src/sonic-grpc/setup.py | 26 ++ src/sonic-grpc/sonic_grpc/__init__.py | 10 + src/sonic-grpc/sonic_grpc/gnoi/__init__.py | 20 + src/sonic-grpc/sonic_grpc/gnoi/client.py | 147 +++++++ src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py | 30 ++ .../sonic_grpc/gnoi/common_pb2_grpc.py | 4 + src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py | 63 +++ .../sonic_grpc/gnoi/file_pb2_grpc.py | 292 ++++++++++++++ src/sonic-grpc/sonic_grpc/gnoi/system_pb2.py | 85 ++++ .../sonic_grpc/gnoi/system_pb2_grpc.py | 369 ++++++++++++++++++ src/sonic-grpc/sonic_grpc/gnoi/testing.py | 258 ++++++++++++ src/sonic-grpc/sonic_grpc/gnoi/types_pb2.py | 42 ++ .../sonic_grpc/gnoi/types_pb2_grpc.py | 4 + 16 files changed, 1365 insertions(+) create mode 100644 rules/sonic-grpc.mk create mode 100644 src/sonic-grpc/setup.py create mode 100644 src/sonic-grpc/sonic_grpc/__init__.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/__init__.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/client.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/common_pb2_grpc.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/file_pb2_grpc.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/system_pb2.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/system_pb2_grpc.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/testing.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/types_pb2.py create mode 100644 src/sonic-grpc/sonic_grpc/gnoi/types_pb2_grpc.py diff --git a/dockers/docker-sonic-mgmt/Dockerfile.j2 b/dockers/docker-sonic-mgmt/Dockerfile.j2 index 430b7f5eb02..67d434c2dca 100755 --- a/dockers/docker-sonic-mgmt/Dockerfile.j2 +++ b/dockers/docker-sonic-mgmt/Dockerfile.j2 @@ -78,6 +78,7 @@ RUN uv pip install --no-cache \ dpugen \ future \ gitpython \ + grpcio \ ipaddr \ ipython \ ixload \ @@ -190,6 +191,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 \ diff --git a/rules/docker-sonic-mgmt.mk b/rules/docker-sonic-mgmt.mk index 49add4c9802..c326c5c89f6 100644 --- a/rules/docker-sonic-mgmt.mk +++ b/rules/docker-sonic-mgmt.mk @@ -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) diff --git a/rules/sonic-grpc.mk b/rules/sonic-grpc.mk new file mode 100644 index 00000000000..db411d45e5b --- /dev/null +++ b/rules/sonic-grpc.mk @@ -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) diff --git a/src/sonic-grpc/setup.py b/src/sonic-grpc/setup.py new file mode 100644 index 00000000000..e0eadf0d0f5 --- /dev/null +++ b/src/sonic-grpc/setup.py @@ -0,0 +1,26 @@ +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', + ], + classifiers=[ + 'Intended Audience :: Developers', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3', + ], + keywords='sonic SONiC gnoi grpc', +) diff --git a/src/sonic-grpc/sonic_grpc/__init__.py b/src/sonic-grpc/sonic_grpc/__init__.py new file mode 100644 index 00000000000..96edaa3b6a9 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/__init__.py @@ -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 +""" diff --git a/src/sonic-grpc/sonic_grpc/gnoi/__init__.py b/src/sonic-grpc/sonic_grpc/gnoi/__init__.py new file mode 100644 index 00000000000..bbd85797372 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/__init__.py @@ -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'] diff --git a/src/sonic-grpc/sonic_grpc/gnoi/client.py b/src/sonic-grpc/sonic_grpc/gnoi/client.py new file mode 100644 index 00000000000..36dad35a86c --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/client.py @@ -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) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py b/src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py new file mode 100644 index 00000000000..f179e6f93b0 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/common_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: github.com/openconfig/gnoi/common/common.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from sonic_grpc.gnoi import types_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_types_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.github.com/openconfig/gnoi/common/common.proto\x12\x0bgnoi.common\x1a,github.com/openconfig/gnoi/types/types.proto\"\xf1\x01\n\x0eRemoteDownload\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x36\n\x08protocol\x18\x02 \x01(\x0e\x32$.gnoi.common.RemoteDownload.Protocol\x12,\n\x0b\x63redentials\x18\x03 \x01(\x0b\x32\x17.gnoi.types.Credentials\x12\x16\n\x0esource_address\x18\x04 \x01(\t\x12\x12\n\nsource_vrf\x18\x05 \x01(\t\"?\n\x08Protocol\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04SFTP\x10\x01\x12\x08\n\x04HTTP\x10\x02\x12\t\n\x05HTTPS\x10\x03\x12\x07\n\x03SCP\x10\x04\x42#Z!github.com/openconfig/gnoi/commonb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.openconfig.gnoi.common.common_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'Z!github.com/openconfig/gnoi/common' + _globals['_REMOTEDOWNLOAD']._serialized_start=110 + _globals['_REMOTEDOWNLOAD']._serialized_end=351 + _globals['_REMOTEDOWNLOAD_PROTOCOL']._serialized_start=288 + _globals['_REMOTEDOWNLOAD_PROTOCOL']._serialized_end=351 +# @@protoc_insertion_point(module_scope) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/common_pb2_grpc.py b/src/sonic-grpc/sonic_grpc/gnoi/common_pb2_grpc.py new file mode 100644 index 00000000000..2daafffebfc --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/common_pb2_grpc.py @@ -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 + diff --git a/src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py b/src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py new file mode 100644 index 00000000000..fab938d9a2f --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/file_pb2.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: github.com/openconfig/gnoi/file/file.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'github.com/openconfig/gnoi/file/file.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from sonic_grpc.gnoi import common_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_common_dot_common__pb2 +from sonic_grpc.gnoi import types_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_types_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*github.com/openconfig/gnoi/file/file.proto\x12\tgnoi.file\x1a.github.com/openconfig/gnoi/common/common.proto\x1a,github.com/openconfig/gnoi/types/types.proto\"\xb5\x01\n\nPutRequest\x12-\n\x04open\x18\x01 \x01(\x0b\x32\x1d.gnoi.file.PutRequest.DetailsH\x00\x12\x12\n\x08\x63ontents\x18\x02 \x01(\x0cH\x00\x12$\n\x04hash\x18\x03 \x01(\x0b\x32\x14.gnoi.types.HashTypeH\x00\x1a\x33\n\x07\x44\x65tails\x12\x13\n\x0bremote_file\x18\x01 \x01(\t\x12\x13\n\x0bpermissions\x18\x02 \x01(\rB\t\n\x07request\"\r\n\x0bPutResponse\"!\n\nGetRequest\x12\x13\n\x0bremote_file\x18\x01 \x01(\t\"S\n\x0bGetResponse\x12\x12\n\x08\x63ontents\x18\x01 \x01(\x0cH\x00\x12$\n\x04hash\x18\x02 \x01(\x0b\x32\x14.gnoi.types.HashTypeH\x00\x42\n\n\x08response\"c\n\x17TransferToRemoteRequest\x12\x12\n\nlocal_path\x18\x01 \x01(\t\x12\x34\n\x0fremote_download\x18\x02 \x01(\x0b\x32\x1b.gnoi.common.RemoteDownload\">\n\x18TransferToRemoteResponse\x12\"\n\x04hash\x18\x01 \x01(\x0b\x32\x14.gnoi.types.HashType\"\x1b\n\x0bStatRequest\x12\x0c\n\x04path\x18\x01 \x01(\t\"2\n\x0cStatResponse\x12\"\n\x05stats\x18\x01 \x03(\x0b\x32\x13.gnoi.file.StatInfo\"a\n\x08StatInfo\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x15\n\rlast_modified\x18\x02 \x01(\x04\x12\x13\n\x0bpermissions\x18\x03 \x01(\r\x12\x0c\n\x04size\x18\x04 \x01(\x04\x12\r\n\x05umask\x18\x05 \x01(\r\"$\n\rRemoveRequest\x12\x13\n\x0bremote_file\x18\x01 \x01(\t\"\x10\n\x0eRemoveResponse2\xd5\x02\n\x04\x46ile\x12\x38\n\x03Get\x12\x15.gnoi.file.GetRequest\x1a\x16.gnoi.file.GetResponse\"\x00\x30\x01\x12]\n\x10TransferToRemote\x12\".gnoi.file.TransferToRemoteRequest\x1a#.gnoi.file.TransferToRemoteResponse\"\x00\x12\x38\n\x03Put\x12\x15.gnoi.file.PutRequest\x1a\x16.gnoi.file.PutResponse\"\x00(\x01\x12\x39\n\x04Stat\x12\x16.gnoi.file.StatRequest\x1a\x17.gnoi.file.StatResponse\"\x00\x12?\n\x06Remove\x12\x18.gnoi.file.RemoveRequest\x1a\x19.gnoi.file.RemoveResponse\"\x00\x42)Z\x1fgithub.com/openconfig/gnoi/file\xd2>\x05\x30.1.0b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.openconfig.gnoi.file.file_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z\037github.com/openconfig/gnoi/file\322>\0050.1.0' + _globals['_PUTREQUEST']._serialized_start=152 + _globals['_PUTREQUEST']._serialized_end=333 + _globals['_PUTREQUEST_DETAILS']._serialized_start=271 + _globals['_PUTREQUEST_DETAILS']._serialized_end=322 + _globals['_PUTRESPONSE']._serialized_start=335 + _globals['_PUTRESPONSE']._serialized_end=348 + _globals['_GETREQUEST']._serialized_start=350 + _globals['_GETREQUEST']._serialized_end=383 + _globals['_GETRESPONSE']._serialized_start=385 + _globals['_GETRESPONSE']._serialized_end=468 + _globals['_TRANSFERTOREMOTEREQUEST']._serialized_start=470 + _globals['_TRANSFERTOREMOTEREQUEST']._serialized_end=569 + _globals['_TRANSFERTOREMOTERESPONSE']._serialized_start=571 + _globals['_TRANSFERTOREMOTERESPONSE']._serialized_end=633 + _globals['_STATREQUEST']._serialized_start=635 + _globals['_STATREQUEST']._serialized_end=662 + _globals['_STATRESPONSE']._serialized_start=664 + _globals['_STATRESPONSE']._serialized_end=714 + _globals['_STATINFO']._serialized_start=716 + _globals['_STATINFO']._serialized_end=813 + _globals['_REMOVEREQUEST']._serialized_start=815 + _globals['_REMOVEREQUEST']._serialized_end=851 + _globals['_REMOVERESPONSE']._serialized_start=853 + _globals['_REMOVERESPONSE']._serialized_end=869 + _globals['_FILE']._serialized_start=872 + _globals['_FILE']._serialized_end=1213 +# @@protoc_insertion_point(module_scope) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/file_pb2_grpc.py b/src/sonic-grpc/sonic_grpc/gnoi/file_pb2_grpc.py new file mode 100644 index 00000000000..029eda46cf8 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/file_pb2_grpc.py @@ -0,0 +1,292 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from sonic_grpc.gnoi import file_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2 + +GRPC_GENERATED_VERSION = '1.70.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in github.com/openconfig/gnoi/file/file_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class FileStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Get = channel.unary_stream( + '/gnoi.file.File/Get', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetResponse.FromString, + _registered_method=True) + self.TransferToRemote = channel.unary_unary( + '/gnoi.file.File/TransferToRemote', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteResponse.FromString, + _registered_method=True) + self.Put = channel.stream_unary( + '/gnoi.file.File/Put', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutResponse.FromString, + _registered_method=True) + self.Stat = channel.unary_unary( + '/gnoi.file.File/Stat', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatResponse.FromString, + _registered_method=True) + self.Remove = channel.unary_unary( + '/gnoi.file.File/Remove', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveResponse.FromString, + _registered_method=True) + + +class FileServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Get(self, request, context): + """Get reads and streams the contents of a file from the target. + The file is streamed by sequential messages, each containing up to + 64KB of data. A final message is sent prior to closing the stream + that contains the hash of the data sent. An error is returned + if the file does not exist or there was an error reading the file. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TransferToRemote(self, request, context): + """TransferToRemote transfers the contents of a file from the target to a + specified remote location. The response contains the hash of the data + transferred. An error is returned if the file does not exist, the file + transfer fails, or if there was an error reading the file. This is a + blocking call until the file transfer is complete. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Put(self, request_iterator, context): + """Put streams data into a file on the target. The file is sent in + sequential messages, each message containing up to 64KB of data. A final + message must be sent that includes the hash of the data sent. An + error is returned if the location does not exist or there is an error + writing the data. If no checksum is received, the target must assume the + operation is incomplete and remove the partially transmitted file. The + target should initially write the file to a temporary location so a failure + does not destroy the original file. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Stat(self, request, context): + """Stat returns metadata about a file on the target. An error is returned + if the file does not exist of there is an error in accessing the metadata. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Remove(self, request, context): + """Remove removes the specified file from the target. An error is + returned if the file does not exist, is a directory, or the remove + operation encounters an error (e.g., permission denied). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_FileServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Get': grpc.unary_stream_rpc_method_handler( + servicer.Get, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetResponse.SerializeToString, + ), + 'TransferToRemote': grpc.unary_unary_rpc_method_handler( + servicer.TransferToRemote, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteResponse.SerializeToString, + ), + 'Put': grpc.stream_unary_rpc_method_handler( + servicer.Put, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutResponse.SerializeToString, + ), + 'Stat': grpc.unary_unary_rpc_method_handler( + servicer.Stat, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatResponse.SerializeToString, + ), + 'Remove': grpc.unary_unary_rpc_method_handler( + servicer.Remove, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'gnoi.file.File', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('gnoi.file.File', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class File(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Get(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/gnoi.file.File/Get', + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.GetResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TransferToRemote(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/gnoi.file.File/TransferToRemote', + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.TransferToRemoteResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Put(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_unary( + request_iterator, + target, + '/gnoi.file.File/Put', + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.PutResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Stat(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/gnoi.file.File/Stat', + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.StatResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Remove(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/gnoi.file.File/Remove', + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_file_dot_file__pb2.RemoveResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/system_pb2.py b/src/sonic-grpc/sonic_grpc/gnoi/system_pb2.py new file mode 100644 index 00000000000..7c50780e2ca --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/system_pb2.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: github.com/openconfig/gnoi/system/system.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from sonic_grpc.gnoi import common_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_common_dot_common__pb2 +from sonic_grpc.gnoi import types_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_types_dot_types__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.github.com/openconfig/gnoi/system/system.proto\x12\x0bgnoi.system\x1a.github.com/openconfig/gnoi/common/common.proto\x1a,github.com/openconfig/gnoi/types/types.proto\"L\n\x1dSwitchControlProcessorRequest\x12+\n\x11\x63ontrol_processor\x18\x01 \x01(\x0b\x32\x10.gnoi.types.Path\"n\n\x1eSwitchControlProcessorResponse\x12+\n\x11\x63ontrol_processor\x18\x01 \x01(\x0b\x32\x10.gnoi.types.Path\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x0e\n\x06uptime\x18\x03 \x01(\x03\"\x92\x01\n\rRebootRequest\x12)\n\x06method\x18\x01 \x01(\x0e\x32\x19.gnoi.system.RebootMethod\x12\r\n\x05\x64\x65lay\x18\x02 \x01(\x04\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\'\n\rsubcomponents\x18\x04 \x03(\x0b\x32\x10.gnoi.types.Path\x12\r\n\x05\x66orce\x18\x05 \x01(\x08\"\x10\n\x0eRebootResponse\"O\n\x13\x43\x61ncelRebootRequest\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\'\n\rsubcomponents\x18\x02 \x03(\x0b\x32\x10.gnoi.types.Path\"\x16\n\x14\x43\x61ncelRebootResponse\">\n\x13RebootStatusRequest\x12\'\n\rsubcomponents\x18\x01 \x03(\x0b\x32\x10.gnoi.types.Path\"\xb7\x01\n\x14RebootStatusResponse\x12\x0e\n\x06\x61\x63tive\x18\x01 \x01(\x08\x12\x0c\n\x04wait\x18\x02 \x01(\x04\x12\x0c\n\x04when\x18\x03 \x01(\x04\x12\x0e\n\x06reason\x18\x04 \x01(\t\x12\r\n\x05\x63ount\x18\x05 \x01(\r\x12)\n\x06method\x18\x06 \x01(\x0e\x32\x19.gnoi.system.RebootMethod\x12)\n\x06status\x18\x07 \x01(\x0b\x32\x19.gnoi.system.RebootStatus\"\xb5\x01\n\x0cRebootStatus\x12\x30\n\x06status\x18\x01 \x01(\x0e\x32 .gnoi.system.RebootStatus.Status\x12\x0f\n\x07message\x18\x02 \x01(\t\"b\n\x06Status\x12\x12\n\x0eSTATUS_UNKNOWN\x10\x00\x12\x12\n\x0eSTATUS_SUCCESS\x10\x01\x12\x1c\n\x18STATUS_RETRIABLE_FAILURE\x10\x02\x12\x12\n\x0eSTATUS_FAILURE\x10\x03\"\r\n\x0bTimeRequest\"\x1c\n\x0cTimeResponse\x12\x0c\n\x04time\x18\x01 \x01(\x04\"\xe6\x01\n\x0bPingRequest\x12\x13\n\x0b\x64\x65stination\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\r\n\x05\x63ount\x18\x03 \x01(\x05\x12\x10\n\x08interval\x18\x04 \x01(\x03\x12\x0c\n\x04wait\x18\x05 \x01(\x03\x12\x0c\n\x04size\x18\x06 \x01(\x05\x12\x17\n\x0f\x64o_not_fragment\x18\x07 \x01(\x08\x12\x16\n\x0e\x64o_not_resolve\x18\x08 \x01(\x08\x12*\n\nl3protocol\x18\t \x01(\x0e\x32\x16.gnoi.types.L3Protocol\x12\x18\n\x10network_instance\x18\n \x01(\t\"\xc1\x01\n\x0cPingResponse\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x0c\n\x04time\x18\x02 \x01(\x03\x12\x0c\n\x04sent\x18\x03 \x01(\x05\x12\x10\n\x08received\x18\x04 \x01(\x05\x12\x10\n\x08min_time\x18\x05 \x01(\x03\x12\x10\n\x08\x61vg_time\x18\x06 \x01(\x03\x12\x10\n\x08max_time\x18\x07 \x01(\x03\x12\x0f\n\x07std_dev\x18\x08 \x01(\x03\x12\r\n\x05\x62ytes\x18\x0b \x01(\x05\x12\x10\n\x08sequence\x18\x0c \x01(\x05\x12\x0b\n\x03ttl\x18\r \x01(\x05\"\xe7\x02\n\x11TracerouteRequest\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x13\n\x0binitial_ttl\x18\x03 \x01(\r\x12\x0f\n\x07max_ttl\x18\x04 \x01(\x05\x12\x0c\n\x04wait\x18\x05 \x01(\x03\x12\x17\n\x0f\x64o_not_fragment\x18\x06 \x01(\x08\x12\x16\n\x0e\x64o_not_resolve\x18\x07 \x01(\x08\x12*\n\nl3protocol\x18\x08 \x01(\x0e\x32\x16.gnoi.types.L3Protocol\x12=\n\nl4protocol\x18\t \x01(\x0e\x32).gnoi.system.TracerouteRequest.L4Protocol\x12\x19\n\x11\x64o_not_lookup_asn\x18\n \x01(\x08\x12\x18\n\x10network_instance\x18\x0b \x01(\t\"(\n\nL4Protocol\x12\x08\n\x04ICMP\x10\x00\x12\x07\n\x03TCP\x10\x01\x12\x07\n\x03UDP\x10\x02\"\xda\x05\n\x12TracerouteResponse\x12\x18\n\x10\x64\x65stination_name\x18\x01 \x01(\t\x12\x1b\n\x13\x64\x65stination_address\x18\x02 \x01(\t\x12\x0c\n\x04hops\x18\x03 \x01(\x05\x12\x13\n\x0bpacket_size\x18\x04 \x01(\x05\x12\x0b\n\x03hop\x18\x05 \x01(\x05\x12\x0f\n\x07\x61\x64\x64ress\x18\x06 \x01(\t\x12\x0c\n\x04name\x18\x07 \x01(\t\x12\x0b\n\x03rtt\x18\x08 \x01(\x03\x12\x34\n\x05state\x18\t \x01(\x0e\x32%.gnoi.system.TracerouteResponse.State\x12\x11\n\ticmp_code\x18\n \x01(\x05\x12\x37\n\x04mpls\x18\x0b \x03(\x0b\x32).gnoi.system.TracerouteResponse.MplsEntry\x12\x0f\n\x07\x61s_path\x18\x0c \x03(\x05\x12\x42\n\ricmp_ext_data\x18\r \x03(\x0b\x32+.gnoi.system.TracerouteResponse.IcmpExtData\x1a\x38\n\x0bIcmpExtData\x12\r\n\x05\x63lass\x18\x01 \x01(\r\x12\x0c\n\x04type\x18\x02 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x03 \x03(\r\x1a+\n\tMplsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xf2\x01\n\x05State\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x08\n\x04NONE\x10\x01\x12\x0b\n\x07UNKNOWN\x10\x02\x12\x08\n\x04ICMP\x10\x03\x12\x14\n\x10HOST_UNREACHABLE\x10\x04\x12\x17\n\x13NETWORK_UNREACHABLE\x10\x05\x12\x18\n\x14PROTOCOL_UNREACHABLE\x10\x06\x12\x17\n\x13SOURCE_ROUTE_FAILED\x10\x07\x12\x18\n\x14\x46RAGMENTATION_NEEDED\x10\x08\x12\x0e\n\nPROHIBITED\x10\t\x12\x18\n\x14PRECEDENCE_VIOLATION\x10\n\x12\x15\n\x11PRECEDENCE_CUTOFF\x10\x0b\"t\n\x07Package\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x10\n\x08\x61\x63tivate\x18\x05 \x01(\x08\x12\x34\n\x0fremote_download\x18\x06 \x01(\x0b\x32\x1b.gnoi.common.RemoteDownload\"\x81\x01\n\x11SetPackageRequest\x12\'\n\x07package\x18\x01 \x01(\x0b\x32\x14.gnoi.system.PackageH\x00\x12\x12\n\x08\x63ontents\x18\x02 \x01(\x0cH\x00\x12$\n\x04hash\x18\x03 \x01(\x0b\x32\x14.gnoi.types.HashTypeH\x00\x42\t\n\x07request\"\x14\n\x12SetPackageResponse\"\xdd\x01\n\x12KillProcessRequest\x12\x0b\n\x03pid\x18\x01 \x01(\r\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x36\n\x06signal\x18\x03 \x01(\x0e\x32&.gnoi.system.KillProcessRequest.Signal\x12\x0f\n\x07restart\x18\x04 \x01(\x08\"c\n\x06Signal\x12\x16\n\x12SIGNAL_UNSPECIFIED\x10\x00\x12\x0f\n\x0bSIGNAL_TERM\x10\x01\x12\x0f\n\x0bSIGNAL_KILL\x10\x02\x12\x0e\n\nSIGNAL_HUP\x10\x03\x12\x0f\n\x0bSIGNAL_ABRT\x10\x04\"\x15\n\x13KillProcessResponse*d\n\x0cRebootMethod\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04\x43OLD\x10\x01\x12\r\n\tPOWERDOWN\x10\x02\x12\x08\n\x04HALT\x10\x03\x12\x08\n\x04WARM\x10\x04\x12\x07\n\x03NSF\x10\x05\x12\x0b\n\x07POWERUP\x10\x07\"\x04\x08\x06\x10\x06\x32\xea\x05\n\x06System\x12?\n\x04Ping\x12\x18.gnoi.system.PingRequest\x1a\x19.gnoi.system.PingResponse\"\x00\x30\x01\x12Q\n\nTraceroute\x12\x1e.gnoi.system.TracerouteRequest\x1a\x1f.gnoi.system.TracerouteResponse\"\x00\x30\x01\x12=\n\x04Time\x12\x18.gnoi.system.TimeRequest\x1a\x19.gnoi.system.TimeResponse\"\x00\x12Q\n\nSetPackage\x12\x1e.gnoi.system.SetPackageRequest\x1a\x1f.gnoi.system.SetPackageResponse\"\x00(\x01\x12s\n\x16SwitchControlProcessor\x12*.gnoi.system.SwitchControlProcessorRequest\x1a+.gnoi.system.SwitchControlProcessorResponse\"\x00\x12\x43\n\x06Reboot\x12\x1a.gnoi.system.RebootRequest\x1a\x1b.gnoi.system.RebootResponse\"\x00\x12U\n\x0cRebootStatus\x12 .gnoi.system.RebootStatusRequest\x1a!.gnoi.system.RebootStatusResponse\"\x00\x12U\n\x0c\x43\x61ncelReboot\x12 .gnoi.system.CancelRebootRequest\x1a!.gnoi.system.CancelRebootResponse\"\x00\x12R\n\x0bKillProcess\x12\x1f.gnoi.system.KillProcessRequest\x1a .gnoi.system.KillProcessResponse\"\x00\x42+Z!github.com/openconfig/gnoi/system\xd2>\x05\x31.4.0b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.openconfig.gnoi.system.system_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'Z!github.com/openconfig/gnoi/system\322>\0051.4.0' + _globals['_TRACEROUTERESPONSE_MPLSENTRY']._options = None + _globals['_TRACEROUTERESPONSE_MPLSENTRY']._serialized_options = b'8\001' + _globals['_REBOOTMETHOD']._serialized_start=3141 + _globals['_REBOOTMETHOD']._serialized_end=3241 + _globals['_SWITCHCONTROLPROCESSORREQUEST']._serialized_start=157 + _globals['_SWITCHCONTROLPROCESSORREQUEST']._serialized_end=233 + _globals['_SWITCHCONTROLPROCESSORRESPONSE']._serialized_start=235 + _globals['_SWITCHCONTROLPROCESSORRESPONSE']._serialized_end=345 + _globals['_REBOOTREQUEST']._serialized_start=348 + _globals['_REBOOTREQUEST']._serialized_end=494 + _globals['_REBOOTRESPONSE']._serialized_start=496 + _globals['_REBOOTRESPONSE']._serialized_end=512 + _globals['_CANCELREBOOTREQUEST']._serialized_start=514 + _globals['_CANCELREBOOTREQUEST']._serialized_end=593 + _globals['_CANCELREBOOTRESPONSE']._serialized_start=595 + _globals['_CANCELREBOOTRESPONSE']._serialized_end=617 + _globals['_REBOOTSTATUSREQUEST']._serialized_start=619 + _globals['_REBOOTSTATUSREQUEST']._serialized_end=681 + _globals['_REBOOTSTATUSRESPONSE']._serialized_start=684 + _globals['_REBOOTSTATUSRESPONSE']._serialized_end=867 + _globals['_REBOOTSTATUS']._serialized_start=870 + _globals['_REBOOTSTATUS']._serialized_end=1051 + _globals['_REBOOTSTATUS_STATUS']._serialized_start=953 + _globals['_REBOOTSTATUS_STATUS']._serialized_end=1051 + _globals['_TIMEREQUEST']._serialized_start=1053 + _globals['_TIMEREQUEST']._serialized_end=1066 + _globals['_TIMERESPONSE']._serialized_start=1068 + _globals['_TIMERESPONSE']._serialized_end=1096 + _globals['_PINGREQUEST']._serialized_start=1099 + _globals['_PINGREQUEST']._serialized_end=1329 + _globals['_PINGRESPONSE']._serialized_start=1332 + _globals['_PINGRESPONSE']._serialized_end=1525 + _globals['_TRACEROUTEREQUEST']._serialized_start=1528 + _globals['_TRACEROUTEREQUEST']._serialized_end=1887 + _globals['_TRACEROUTEREQUEST_L4PROTOCOL']._serialized_start=1847 + _globals['_TRACEROUTEREQUEST_L4PROTOCOL']._serialized_end=1887 + _globals['_TRACEROUTERESPONSE']._serialized_start=1890 + _globals['_TRACEROUTERESPONSE']._serialized_end=2620 + _globals['_TRACEROUTERESPONSE_ICMPEXTDATA']._serialized_start=2274 + _globals['_TRACEROUTERESPONSE_ICMPEXTDATA']._serialized_end=2330 + _globals['_TRACEROUTERESPONSE_MPLSENTRY']._serialized_start=2332 + _globals['_TRACEROUTERESPONSE_MPLSENTRY']._serialized_end=2375 + _globals['_TRACEROUTERESPONSE_STATE']._serialized_start=2378 + _globals['_TRACEROUTERESPONSE_STATE']._serialized_end=2620 + _globals['_PACKAGE']._serialized_start=2622 + _globals['_PACKAGE']._serialized_end=2738 + _globals['_SETPACKAGEREQUEST']._serialized_start=2741 + _globals['_SETPACKAGEREQUEST']._serialized_end=2870 + _globals['_SETPACKAGERESPONSE']._serialized_start=2872 + _globals['_SETPACKAGERESPONSE']._serialized_end=2892 + _globals['_KILLPROCESSREQUEST']._serialized_start=2895 + _globals['_KILLPROCESSREQUEST']._serialized_end=3116 + _globals['_KILLPROCESSREQUEST_SIGNAL']._serialized_start=3017 + _globals['_KILLPROCESSREQUEST_SIGNAL']._serialized_end=3116 + _globals['_KILLPROCESSRESPONSE']._serialized_start=3118 + _globals['_KILLPROCESSRESPONSE']._serialized_end=3139 + _globals['_SYSTEM']._serialized_start=3244 + _globals['_SYSTEM']._serialized_end=3990 +# @@protoc_insertion_point(module_scope) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/system_pb2_grpc.py b/src/sonic-grpc/sonic_grpc/gnoi/system_pb2_grpc.py new file mode 100644 index 00000000000..8d4b912842d --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/system_pb2_grpc.py @@ -0,0 +1,369 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from sonic_grpc.gnoi import system_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2 + + +class SystemStub(object): + """The gNOI service is a collection of operational RPC's that allow for the + management of a target outside of the configuration and telemetry pipeline. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Ping = channel.unary_stream( + '/gnoi.system.System/Ping', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingResponse.FromString, + ) + self.Traceroute = channel.unary_stream( + '/gnoi.system.System/Traceroute', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteResponse.FromString, + ) + self.Time = channel.unary_unary( + '/gnoi.system.System/Time', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeResponse.FromString, + ) + self.SetPackage = channel.stream_unary( + '/gnoi.system.System/SetPackage', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageResponse.FromString, + ) + self.SwitchControlProcessor = channel.unary_unary( + '/gnoi.system.System/SwitchControlProcessor', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorResponse.FromString, + ) + self.Reboot = channel.unary_unary( + '/gnoi.system.System/Reboot', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootResponse.FromString, + ) + self.RebootStatus = channel.unary_unary( + '/gnoi.system.System/RebootStatus', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusResponse.FromString, + ) + self.CancelReboot = channel.unary_unary( + '/gnoi.system.System/CancelReboot', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootResponse.FromString, + ) + self.KillProcess = channel.unary_unary( + '/gnoi.system.System/KillProcess', + request_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessRequest.SerializeToString, + response_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessResponse.FromString, + ) + + +class SystemServicer(object): + """The gNOI service is a collection of operational RPC's that allow for the + management of a target outside of the configuration and telemetry pipeline. + """ + + def Ping(self, request, context): + """Ping executes the ping command on the target and streams back + the results. Some targets may not stream any results until all + results are in. The stream should provide single ping packet responses + and must provide summary statistics. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Traceroute(self, request, context): + """Traceroute executes the traceroute command on the target and streams back + the results. Some targets may not stream any results until all + results are in. If a hop count is not explicitly provided, + 30 is used. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Time(self, request, context): + """Time returns the current time on the target. Time is typically used to + test if a target is actually responding. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SetPackage(self, request_iterator, context): + """SetPackage places a software package (possibly including bootable images) + on the target. The file is sent in sequential messages, each message + up to 64KB of data. A final message must be sent that includes the hash + of the data sent. An error is returned if the location does not exist or + there is an error writing the data. If no checksum is received, the target + must assume the operation is incomplete and remove the partially + transmitted file. The target should initially write the file to a temporary + location so a failure does not destroy the original file. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SwitchControlProcessor(self, request, context): + """SwitchControlProcessor will switch from the current route processor to the + provided route processor. If the current route processor is the same as the + one provided it is a NOOP. If the target does not exist an error is + returned. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Reboot(self, request, context): + """Reboot causes the target to reboot, possibly at some point in the future. + If the method of reboot is not supported then the Reboot RPC will fail. + If the reboot is immediate the command will block until the subcomponents + have restarted. + If a reboot on the active control processor is pending the service must + reject all other reboot requests. + If a reboot request for active control processor is initiated with other + pending reboot requests it must be rejected. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RebootStatus(self, request, context): + """RebootStatus returns the status of reboot for the target. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def CancelReboot(self, request, context): + """CancelReboot cancels any pending reboot request. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def KillProcess(self, request, context): + """KillProcess kills an OS process and optionally restarts it. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_SystemServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Ping': grpc.unary_stream_rpc_method_handler( + servicer.Ping, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingResponse.SerializeToString, + ), + 'Traceroute': grpc.unary_stream_rpc_method_handler( + servicer.Traceroute, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteResponse.SerializeToString, + ), + 'Time': grpc.unary_unary_rpc_method_handler( + servicer.Time, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeResponse.SerializeToString, + ), + 'SetPackage': grpc.stream_unary_rpc_method_handler( + servicer.SetPackage, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageResponse.SerializeToString, + ), + 'SwitchControlProcessor': grpc.unary_unary_rpc_method_handler( + servicer.SwitchControlProcessor, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorResponse.SerializeToString, + ), + 'Reboot': grpc.unary_unary_rpc_method_handler( + servicer.Reboot, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootResponse.SerializeToString, + ), + 'RebootStatus': grpc.unary_unary_rpc_method_handler( + servicer.RebootStatus, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusResponse.SerializeToString, + ), + 'CancelReboot': grpc.unary_unary_rpc_method_handler( + servicer.CancelReboot, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootResponse.SerializeToString, + ), + 'KillProcess': grpc.unary_unary_rpc_method_handler( + servicer.KillProcess, + request_deserializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessRequest.FromString, + response_serializer=github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'gnoi.system.System', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class System(object): + """The gNOI service is a collection of operational RPC's that allow for the + management of a target outside of the configuration and telemetry pipeline. + """ + + @staticmethod + def Ping(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/gnoi.system.System/Ping', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.PingResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Traceroute(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/gnoi.system.System/Traceroute', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TracerouteResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Time(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/Time', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.TimeResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SetPackage(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_unary(request_iterator, target, '/gnoi.system.System/SetPackage', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SetPackageResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SwitchControlProcessor(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/SwitchControlProcessor', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.SwitchControlProcessorResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Reboot(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/Reboot', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def RebootStatus(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/RebootStatus', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.RebootStatusResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def CancelReboot(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/CancelReboot', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.CancelRebootResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def KillProcess(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gnoi.system.System/KillProcess', + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessRequest.SerializeToString, + github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2.KillProcessResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/testing.py b/src/sonic-grpc/sonic_grpc/gnoi/testing.py new file mode 100644 index 00000000000..965f8a83140 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/testing.py @@ -0,0 +1,258 @@ +""" +Fake gNOI gRPC server for testing. + +Spins up a real gRPC server on localhost with controllable service +implementations. Tests use the real GnoiClient against this server — +no mocking of gRPC internals. + +Usage: + from sonic_grpc.gnoi.testing import FakeGnoiServer + from sonic_grpc.gnoi import GnoiClient, system_pb2 + + server = FakeGnoiServer() + server.start() + + # Configure responses + server.system.set_reboot_response() # default success + server.system.set_reboot_status(active=False, status=system_pb2.RebootStatus.Status.STATUS_SUCCESS) + + # Test with real client + with GnoiClient(server.target) as client: + client.system.Reboot(system_pb2.RebootRequest(method=system_pb2.HALT), timeout=5) + resp = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + assert not resp.active + + server.stop() +""" + +from concurrent import futures +import grpc + +from sonic_grpc.gnoi import system_pb2 +from sonic_grpc.gnoi import system_pb2_grpc + + +class FakeSystemServicer(system_pb2_grpc.SystemServicer): + """Fake gNOI System servicer with configurable responses. + + Each RPC returns a configured response or raises a configured error. + Supports response sequences for polling tests. + Call history is recorded for assertions. + """ + + def __init__(self): + self.reset() + + def reset(self): + """Reset all configured responses and call history.""" + self._reboot_response = system_pb2.RebootResponse() + self._reboot_error = None + + self._reboot_status_responses = None # None = use single response + self._reboot_status_response = self._default_status_response() + self._reboot_status_error = None + + self._cancel_reboot_response = system_pb2.CancelRebootResponse() + self._cancel_reboot_error = None + + self.reboot_calls = [] + self.reboot_status_calls = [] + self.cancel_reboot_calls = [] + + @staticmethod + def _default_status_response(): + resp = system_pb2.RebootStatusResponse() + resp.active = False + resp.status.status = system_pb2.RebootStatus.Status.STATUS_SUCCESS + return resp + + # ---- Configuration ---- + + def set_reboot_response(self, response=None, error_code=None, error_message=""): + """Set Reboot RPC response or error. + + Args: + response: RebootResponse proto (default: empty success). + error_code: grpc.StatusCode to return as error (e.g. grpc.StatusCode.UNAVAILABLE). + error_message: Error detail string. + """ + if response is not None: + self._reboot_response = response + self._reboot_error = (error_code, error_message) if error_code else None + + def set_reboot_status(self, active=None, status=None, message=None, + response=None, error_code=None, error_message=""): + """Set RebootStatus single response. + + Args: + active: Whether reboot is in progress. + status: RebootStatus.Status enum value. + message: Status message. + response: Full RebootStatusResponse (overrides field args). + error_code: grpc.StatusCode for error response. + error_message: Error detail string. + """ + self._reboot_status_responses = None + if response is not None: + self._reboot_status_response = response + else: + if active is not None: + self._reboot_status_response.active = active + if status is not None: + self._reboot_status_response.status.status = status + if message is not None: + self._reboot_status_response.status.message = message + self._reboot_status_error = (error_code, error_message) if error_code else None + + def set_reboot_status_sequence(self, responses): + """Set a sequence of RebootStatus responses for polling tests. + + Each call pops the next item. Items can be: + - RebootStatusResponse proto → returned as success + - (grpc.StatusCode, message) tuple → returned as error + + After exhaustion, falls back to the single response. + + Args: + responses: List of RebootStatusResponse or (StatusCode, msg) tuples. + """ + self._reboot_status_responses = list(responses) + + def set_cancel_reboot_response(self, response=None, error_code=None, error_message=""): + """Set CancelReboot RPC response or error.""" + if response is not None: + self._cancel_reboot_response = response + self._cancel_reboot_error = (error_code, error_message) if error_code else None + + # ---- RPC implementations ---- + + def Reboot(self, request, context): + self.reboot_calls.append(request) + if self._reboot_error: + context.abort(self._reboot_error[0], self._reboot_error[1]) + return self._reboot_response + + def RebootStatus(self, request, context): + self.reboot_status_calls.append(request) + if self._reboot_status_responses is not None and self._reboot_status_responses: + item = self._reboot_status_responses.pop(0) + if isinstance(item, tuple): + context.abort(item[0], item[1]) + return item + # No sequence configured, or sequence has been exhausted — fall + # through to the single configured response (or error). + if self._reboot_status_error: + context.abort(self._reboot_status_error[0], self._reboot_status_error[1]) + return self._reboot_status_response + + def CancelReboot(self, request, context): + self.cancel_reboot_calls.append(request) + if self._cancel_reboot_error: + context.abort(self._cancel_reboot_error[0], self._cancel_reboot_error[1]) + return self._cancel_reboot_response + + +class FakeGnoiServer: + """Fake gNOI gRPC server for integration-style tests. + + Starts a real gRPC server on a random localhost port. + Tests use GnoiClient(server.target) for real channel communication. + + Example: + server = FakeGnoiServer() + server.start() + try: + with GnoiClient(server.target) as client: + client.system.Reboot(req, timeout=5) + assert len(server.system.reboot_calls) == 1 + finally: + server.stop() + + Or as context manager: + with FakeGnoiServer() as server: + with GnoiClient(server.target) as client: + ... + """ + + def __init__(self, max_workers=2): + self._max_workers = max_workers + self._server = None + self._port = None + self.system = FakeSystemServicer() + + @property + def target(self): + """gRPC target string, e.g. 'localhost:50051'. + + Raises: + RuntimeError: If accessed before ``start()`` has bound a port. + """ + if self._port is None: + raise RuntimeError( + "FakeGnoiServer.target accessed before start() — call " + "start() (or use as a context manager) first." + ) + return f"localhost:{self._port}" + + def start(self): + """Start the fake gRPC server on a random port. + + Raises: + RuntimeError: If the server is already started (use ``stop()`` + first or just rely on the context manager), or if the + listening port could not be bound. + """ + if self._server is not None: + raise RuntimeError( + "FakeGnoiServer.start() called on an already-started server; " + "call stop() first or use a fresh instance." + ) + self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=self._max_workers)) + system_pb2_grpc.add_SystemServicer_to_server(self.system, self._server) + port = self._server.add_insecure_port("localhost:0") + if port == 0: + # add_insecure_port returns 0 on bind failure. + self._server = None + raise RuntimeError( + "FakeGnoiServer.start(): add_insecure_port('localhost:0') failed" + ) + self._port = port + self._server.start() + return self + + def stop(self, grace=0): + """Stop the server and wait for graceful termination. + + gRPC's ``server.stop(grace)`` returns a ``threading.Event`` that is + set once all RPCs have completed. Wait on it so callers (e.g. unit + tests) don't leak server threads / sockets across iterations. + + Raises: + RuntimeError: If the server fails to terminate within the + bounded wait (5s), indicating leaked threads / sockets. + """ + if self._server: + stopped = self._server.stop(grace) + # Bounded wait: grace=0 means "abort in-flight RPCs immediately", + # so the Event fires almost instantly. Cap the wait so a broken + # gRPC build can't hang a test run indefinitely. + if not stopped.wait(timeout=5): + # Don't silently clear state — the server is leaking. Surface + # it so the caller (typically a test) can fail loudly. + raise RuntimeError( + "FakeGnoiServer.stop(): gRPC server did not terminate " + "within 5s; the server is leaking threads / sockets." + ) + self._server = None + self._port = None + + def reset(self): + """Reset all service state (responses + call history).""" + self.system.reset() + + def __enter__(self): + return self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + return False diff --git a/src/sonic-grpc/sonic_grpc/gnoi/types_pb2.py b/src/sonic-grpc/sonic_grpc/gnoi/types_pb2.py new file mode 100644 index 00000000000..5f22d2aca45 --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/types_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: github.com/openconfig/gnoi/types/types.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,github.com/openconfig/gnoi/types/types.proto\x12\ngnoi.types\x1a google/protobuf/descriptor.proto\"\x89\x01\n\x08HashType\x12/\n\x06method\x18\x01 \x01(\x0e\x32\x1f.gnoi.types.HashType.HashMethod\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\">\n\nHashMethod\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06SHA256\x10\x01\x12\n\n\x06SHA512\x10\x02\x12\x07\n\x03MD5\x10\x03\":\n\x04Path\x12\x0e\n\x06origin\x18\x02 \x01(\t\x12\"\n\x04\x65lem\x18\x03 \x03(\x0b\x32\x14.gnoi.types.PathElem\"p\n\x08PathElem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12*\n\x03key\x18\x02 \x03(\x0b\x32\x1d.gnoi.types.PathElem.KeyEntry\x1a*\n\x08KeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"h\n\x0b\x43redentials\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x13\n\tcleartext\x18\x02 \x01(\tH\x00\x12&\n\x06hashed\x18\x03 \x01(\x0b\x32\x14.gnoi.types.HashTypeH\x00\x42\n\n\x08password*1\n\nL3Protocol\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x08\n\x04IPV4\x10\x01\x12\x08\n\x04IPV6\x10\x02:3\n\x0cgnoi_version\x12\x1c.google.protobuf.FileOptions\x18\xea\x07 \x01(\tB\"Z github.com/openconfig/gnoi/typesb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'github.com.openconfig.gnoi.types.types_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'Z github.com/openconfig/gnoi/types' + _globals['_PATHELEM_KEYENTRY']._options = None + _globals['_PATHELEM_KEYENTRY']._serialized_options = b'8\001' + _globals['_L3PROTOCOL']._serialized_start=514 + _globals['_L3PROTOCOL']._serialized_end=563 + _globals['_HASHTYPE']._serialized_start=95 + _globals['_HASHTYPE']._serialized_end=232 + _globals['_HASHTYPE_HASHMETHOD']._serialized_start=170 + _globals['_HASHTYPE_HASHMETHOD']._serialized_end=232 + _globals['_PATH']._serialized_start=234 + _globals['_PATH']._serialized_end=292 + _globals['_PATHELEM']._serialized_start=294 + _globals['_PATHELEM']._serialized_end=406 + _globals['_PATHELEM_KEYENTRY']._serialized_start=364 + _globals['_PATHELEM_KEYENTRY']._serialized_end=406 + _globals['_CREDENTIALS']._serialized_start=408 + _globals['_CREDENTIALS']._serialized_end=512 +# @@protoc_insertion_point(module_scope) diff --git a/src/sonic-grpc/sonic_grpc/gnoi/types_pb2_grpc.py b/src/sonic-grpc/sonic_grpc/gnoi/types_pb2_grpc.py new file mode 100644 index 00000000000..2daafffebfc --- /dev/null +++ b/src/sonic-grpc/sonic_grpc/gnoi/types_pb2_grpc.py @@ -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 + From 5b7827336ac4763b5f515949f4100483e2483f7c Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Thu, 9 Jul 2026 17:09:46 -0500 Subject: [PATCH 2/4] [sonic-grpc]: Import copy_files macro in docker-sonic-mgmt Dockerfile The wheel-install block uses the copy_files macro, which must be imported at the top of the Dockerfile.j2 (as every other wheel-consuming Dockerfile does). Without the import the j2 template fails to render. Add the import. Signed-off-by: Dawei Huang --- dockers/docker-sonic-mgmt/Dockerfile.j2 | 1 + 1 file changed, 1 insertion(+) diff --git a/dockers/docker-sonic-mgmt/Dockerfile.j2 b/dockers/docker-sonic-mgmt/Dockerfile.j2 index 67d434c2dca..18d3b1e8188 100755 --- a/dockers/docker-sonic-mgmt/Dockerfile.j2 +++ b/dockers/docker-sonic-mgmt/Dockerfile.j2 @@ -1,3 +1,4 @@ +{% from "dockers/dockerfile-macros.j2" import copy_files %} {% set prefix = DEFAULT_CONTAINER_REGISTRY %} FROM {{ prefix }}ubuntu:24.04 From 8addce85411768e15ee7782d6fc77901a22dad2e Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Thu, 9 Jul 2026 17:15:16 -0500 Subject: [PATCH 3/4] [sonic-grpc]: Add rules/sonic-grpc.dep for build-cache tracking Every buildable component with GIT_CONTENT_SHA caching has a matching .dep file (included via Makefile.cache) that declares its DEP_FILES and cache mode. Add rules/sonic-grpc.dep for the new SONIC_GRPC_PY3 wheel, mirroring rules/sonic-py-common.dep, so the wheel participates in content-SHA build caching and rebuilds when its sources change. Signed-off-by: Dawei Huang --- rules/sonic-grpc.dep | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 rules/sonic-grpc.dep diff --git a/rules/sonic-grpc.dep b/rules/sonic-grpc.dep new file mode 100644 index 00000000000..f6434bb0517 --- /dev/null +++ b/rules/sonic-grpc.dep @@ -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) From d38cf2aabe249bad06be18360f7252564edebde3 Mon Sep 17 00:00:00 2001 From: Dawei Huang Date: Fri, 10 Jul 2026 11:45:21 -0500 Subject: [PATCH 4/4] [sonic-grpc]: Add unit tests (required by the wheel build test step) The SONiC bookworm/trixie wheel build rule runs `pytest` on each wheel's source unless _TEST=n. sonic-grpc had no tests, so pytest collected 0 items and the wheel target failed. Add the gNOI client and FakeGnoiServer unit tests (31 tests, lifted from the framework in #27760) under src/sonic-grpc/tests/, and declare a `testing` extra so the build's `pip install ".[testing]"` step is satisfied. Verified: `target/python-wheels/bookworm/sonic_grpc-1.0-py3-none-any.whl` now builds with `31 passed` in the test step. Signed-off-by: Dawei Huang --- src/sonic-grpc/setup.py | 3 + src/sonic-grpc/tests/__init__.py | 0 src/sonic-grpc/tests/test_gnoi_client.py | 175 ++++++++++++ src/sonic-grpc/tests/test_gnoi_testing.py | 311 ++++++++++++++++++++++ 4 files changed, 489 insertions(+) create mode 100644 src/sonic-grpc/tests/__init__.py create mode 100644 src/sonic-grpc/tests/test_gnoi_client.py create mode 100644 src/sonic-grpc/tests/test_gnoi_testing.py diff --git a/src/sonic-grpc/setup.py b/src/sonic-grpc/setup.py index e0eadf0d0f5..95001439015 100644 --- a/src/sonic-grpc/setup.py +++ b/src/sonic-grpc/setup.py @@ -17,6 +17,9 @@ 'grpcio', 'protobuf>=4.21', ], + extras_require={ + 'testing': ['pytest'], + }, classifiers=[ 'Intended Audience :: Developers', 'Operating System :: POSIX :: Linux', diff --git a/src/sonic-grpc/tests/__init__.py b/src/sonic-grpc/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sonic-grpc/tests/test_gnoi_client.py b/src/sonic-grpc/tests/test_gnoi_client.py new file mode 100644 index 00000000000..2a66eed4d8c --- /dev/null +++ b/src/sonic-grpc/tests/test_gnoi_client.py @@ -0,0 +1,175 @@ +"""Tests for sonic_grpc.gnoi.client.GnoiClient. + +Uses FakeGnoiServer for real gRPC — no mocking. +""" + +import sys +import unittest + +if sys.version_info[0] < 3: + # sonic_grpc.gnoi is only packaged for Py3 (see setup.py), + # and grpcio dropped Py2 support years ago. Skip the whole module on + # Py2 so the legacy ENABLE_PY2_MODULES=y wheel build doesn't fail on + # an unsatisfiable import. ``raise SkipTest`` at module level is + # recognized by both ``unittest`` and ``pytest`` collection. + raise unittest.SkipTest("sonic_grpc.gnoi requires Python 3") + +from unittest import mock # noqa: E402 +import grpc # noqa: E402 + +from sonic_grpc.gnoi.testing import FakeGnoiServer # noqa: E402 +from sonic_grpc.gnoi.client import GnoiClient # noqa: E402 +from sonic_grpc.gnoi import system_pb2 # noqa: E402 + + +class TestGnoiClient(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.server = FakeGnoiServer() + cls.server.start() + + @classmethod + def tearDownClass(cls): + cls.server.stop() + + def setUp(self): + self.server.reset() + + def test_context_manager_creates_and_closes_channel(self): + with GnoiClient(self.server.target) as client: + self.assertIsNotNone(client.channel) + # After exit, channel is None + self.assertIsNone(client.channel) + + def test_close_idempotent(self): + client = GnoiClient(self.server.target) + client.__enter__() + client.close() + client.close() # Should not raise + + def test_channel_not_available_before_enter(self): + client = GnoiClient(self.server.target) + self.assertIsNone(client.channel) + + def test_system_reboot_rpc(self): + with GnoiClient(self.server.target) as client: + resp = client.system.Reboot( + system_pb2.RebootRequest(method=system_pb2.HALT), + timeout=5, + ) + self.assertIsNotNone(resp) + self.assertEqual(len(self.server.system.reboot_calls), 1) + + def test_system_reboot_status_rpc(self): + with GnoiClient(self.server.target) as client: + resp = client.system.RebootStatus( + system_pb2.RebootStatusRequest(), timeout=5 + ) + self.assertFalse(resp.active) + + def test_grpc_error_propagation(self): + self.server.system.set_reboot_response( + error_code=grpc.StatusCode.UNAVAILABLE, + error_message="connection refused", + ) + with GnoiClient(self.server.target) as client: + with self.assertRaises(grpc.RpcError) as ctx: + client.system.Reboot(system_pb2.RebootRequest(), timeout=5) + self.assertEqual(ctx.exception.code(), grpc.StatusCode.UNAVAILABLE) + + def test_channel_options_accepted(self): + """GnoiClient accepts gRPC channel options.""" + opts = [('grpc.keepalive_time_ms', 10000)] + with GnoiClient(self.server.target, options=opts) as client: + resp = client.system.RebootStatus( + system_pb2.RebootStatusRequest(), timeout=5 + ) + self.assertIsNotNone(resp) + + def test_multiple_rpcs_on_same_channel(self): + with GnoiClient(self.server.target) as client: + client.system.Reboot(system_pb2.RebootRequest(), timeout=5) + client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + client.system.CancelReboot(system_pb2.CancelRebootRequest(), timeout=5) + self.assertEqual(len(self.server.system.reboot_calls), 1) + self.assertEqual(len(self.server.system.reboot_status_calls), 1) + self.assertEqual(len(self.server.system.cancel_reboot_calls), 1) + + def test_credentials_none_uses_insecure_channel(self): + """When credentials is None, GnoiClient opens an insecure channel.""" + with mock.patch( + "sonic_grpc.gnoi.client.grpc.insecure_channel" + ) as m_insecure, mock.patch( + "sonic_grpc.gnoi.client.grpc.secure_channel" + ) as m_secure: + m_insecure.return_value = mock.MagicMock() + client = GnoiClient(self.server.target) + client.__enter__() + client.close() + m_insecure.assert_called_once() + m_secure.assert_not_called() + + def test_credentials_provided_uses_secure_channel(self): + """When credentials is provided, GnoiClient opens a secure channel.""" + creds = grpc.ssl_channel_credentials() + with mock.patch( + "sonic_grpc.gnoi.client.grpc.insecure_channel" + ) as m_insecure, mock.patch( + "sonic_grpc.gnoi.client.grpc.secure_channel" + ) as m_secure: + m_secure.return_value = mock.MagicMock() + client = GnoiClient("dut:50052", credentials=creds) + client.__enter__() + client.close() + m_secure.assert_called_once() + # First positional arg is the target, second is the credentials + args, kwargs = m_secure.call_args + self.assertEqual(args[0], "dut:50052") + self.assertIs(args[1], creds) + m_insecure.assert_not_called() + + def test_unix_socket_target_uses_insecure_channel(self): + """unix:// targets are passed through to insecure_channel as-is. + + gRPC supports the ``unix://`` scheme natively for local IPC, so no + special handling is needed in GnoiClient — but pin the behavior so + a future refactor doesn't accidentally break it. + """ + target = "unix:///var/run/gnmi/gnmi.sock" + with mock.patch( + "sonic_grpc.gnoi.client.grpc.insecure_channel" + ) as m_insecure, mock.patch( + "sonic_grpc.gnoi.client.grpc.secure_channel" + ) as m_secure: + m_insecure.return_value = mock.MagicMock() + client = GnoiClient(target) + client.__enter__() + client.close() + m_insecure.assert_called_once() + args, kwargs = m_insecure.call_args + self.assertEqual(args[0], target) + m_secure.assert_not_called() + + def test_service_stub_before_open_raises(self): + """Accessing a service stub before __enter__ raises a clear error. + + Without the explicit guard, callers got a confusing AttributeError + from inside the generated stub code when self._channel was None. + """ + client = GnoiClient(self.server.target) + with self.assertRaises(RuntimeError) as ctx: + client.system # noqa: B018 — accessing the property is the test + self.assertIn("context manager", str(ctx.exception)) + + def test_service_stub_after_close_raises(self): + """Accessing a service stub after close() raises a clear error.""" + client = GnoiClient(self.server.target) + client.__enter__() + client.close() + with self.assertRaises(RuntimeError): + client.system # noqa: B018 + + +if __name__ == '__main__': + unittest.main() diff --git a/src/sonic-grpc/tests/test_gnoi_testing.py b/src/sonic-grpc/tests/test_gnoi_testing.py new file mode 100644 index 00000000000..cae90d5ae80 --- /dev/null +++ b/src/sonic-grpc/tests/test_gnoi_testing.py @@ -0,0 +1,311 @@ +"""Tests for sonic_grpc.gnoi.testing (FakeGnoiServer). + +These tests use a real gRPC server and real GnoiClient — no mocking. +""" + +import sys +import unittest + +if sys.version_info[0] < 3: + # See sibling test_gnoi_client.py — module is Py3-only. + raise unittest.SkipTest("sonic_grpc.gnoi requires Python 3") + +import grpc # noqa: E402 + +from sonic_grpc.gnoi.testing import FakeGnoiServer # noqa: E402 +from sonic_grpc.gnoi.client import GnoiClient # noqa: E402 +from sonic_grpc.gnoi import system_pb2 # noqa: E402 + + +class TestFakeSystemServicer(unittest.TestCase): + """Unit tests for FakeSystemServicer behavior.""" + + @classmethod + def setUpClass(cls): + cls.server = FakeGnoiServer() + cls.server.start() + + @classmethod + def tearDownClass(cls): + cls.server.stop() + + def setUp(self): + self.server.reset() + + def test_reboot_default_success(self): + with GnoiClient(self.server.target) as client: + resp = client.system.Reboot( + system_pb2.RebootRequest(method=system_pb2.HALT, message="test"), + timeout=5, + ) + self.assertIsNotNone(resp) + self.assertEqual(len(self.server.system.reboot_calls), 1) + self.assertEqual(self.server.system.reboot_calls[0].method, system_pb2.HALT) + self.assertEqual(self.server.system.reboot_calls[0].message, "test") + + def test_reboot_error(self): + self.server.system.set_reboot_response( + error_code=grpc.StatusCode.UNAVAILABLE, + error_message="DPU unreachable", + ) + with GnoiClient(self.server.target) as client: + with self.assertRaises(grpc.RpcError) as ctx: + client.system.Reboot(system_pb2.RebootRequest(), timeout=5) + self.assertEqual(ctx.exception.code(), grpc.StatusCode.UNAVAILABLE) + self.assertIn("DPU unreachable", ctx.exception.details()) + + def test_reboot_status_default_complete(self): + with GnoiClient(self.server.target) as client: + resp = client.system.RebootStatus( + system_pb2.RebootStatusRequest(), timeout=5 + ) + self.assertFalse(resp.active) + self.assertEqual( + resp.status.status, + system_pb2.RebootStatus.Status.STATUS_SUCCESS, + ) + + def test_reboot_status_active(self): + self.server.system.set_reboot_status(active=True) + with GnoiClient(self.server.target) as client: + resp = client.system.RebootStatus( + system_pb2.RebootStatusRequest(), timeout=5 + ) + self.assertTrue(resp.active) + + def test_reboot_status_sequence(self): + """Polling scenario: active → active → done.""" + active_resp = system_pb2.RebootStatusResponse() + active_resp.active = True + + done_resp = system_pb2.RebootStatusResponse() + done_resp.active = False + done_resp.status.status = system_pb2.RebootStatus.Status.STATUS_SUCCESS + + self.server.system.set_reboot_status_sequence([active_resp, active_resp, done_resp]) + + with GnoiClient(self.server.target) as client: + r1 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertTrue(r1.active) + r2 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertTrue(r2.active) + r3 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertFalse(r3.active) + + self.assertEqual(len(self.server.system.reboot_status_calls), 3) + + def test_reboot_status_sequence_with_error(self): + """Polling with transient error then success.""" + done_resp = system_pb2.RebootStatusResponse() + done_resp.active = False + done_resp.status.status = system_pb2.RebootStatus.Status.STATUS_SUCCESS + + self.server.system.set_reboot_status_sequence([ + (grpc.StatusCode.UNAVAILABLE, "transient"), + done_resp, + ]) + + with GnoiClient(self.server.target) as client: + with self.assertRaises(grpc.RpcError): + client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + r2 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertFalse(r2.active) + + def test_reboot_status_error(self): + self.server.system.set_reboot_status( + error_code=grpc.StatusCode.INTERNAL, + error_message="internal failure", + ) + with GnoiClient(self.server.target) as client: + with self.assertRaises(grpc.RpcError) as ctx: + client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertEqual(ctx.exception.code(), grpc.StatusCode.INTERNAL) + + def test_cancel_reboot(self): + with GnoiClient(self.server.target) as client: + resp = client.system.CancelReboot( + system_pb2.CancelRebootRequest(message="abort"), timeout=5 + ) + self.assertIsNotNone(resp) + self.assertEqual(len(self.server.system.cancel_reboot_calls), 1) + + def test_reset_clears_history(self): + with GnoiClient(self.server.target) as client: + client.system.Reboot(system_pb2.RebootRequest(), timeout=5) + self.assertEqual(len(self.server.system.reboot_calls), 1) + self.server.reset() + self.assertEqual(len(self.server.system.reboot_calls), 0) + + +class TestFakeGnoiServer(unittest.TestCase): + """Test server lifecycle.""" + + def test_context_manager(self): + with FakeGnoiServer() as server: + self.assertIn("localhost:", server.target) + with GnoiClient(server.target) as client: + resp = client.system.RebootStatus( + system_pb2.RebootStatusRequest(), timeout=5 + ) + self.assertFalse(resp.active) + + def test_end_to_end_reboot_flow(self): + """Full reboot + poll flow like gnoi_shutdown_daemon.""" + with FakeGnoiServer() as server: + active = system_pb2.RebootStatusResponse() + active.active = True + done = system_pb2.RebootStatusResponse() + done.active = False + done.status.status = system_pb2.RebootStatus.Status.STATUS_SUCCESS + server.system.set_reboot_status_sequence([active, done]) + + with GnoiClient(server.target) as client: + # Send reboot + client.system.Reboot( + system_pb2.RebootRequest(method=system_pb2.HALT, message="shutdown"), + timeout=5, + ) + # Poll + r1 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertTrue(r1.active) + r2 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertFalse(r2.active) + + self.assertEqual(len(server.system.reboot_calls), 1) + self.assertEqual(server.system.reboot_calls[0].method, system_pb2.HALT) + self.assertEqual(len(server.system.reboot_status_calls), 2) + + +class TestFakeGnoiServerGuards(unittest.TestCase): + """Tests for FakeGnoiServer lifecycle / misuse guards.""" + + def test_target_before_start_raises(self): + """target accessed before start() raises a clear error. + + Previously returned the misleading string 'localhost:None', which + manifested as a confusing connection error rather than a misuse + diagnostic. + """ + server = FakeGnoiServer() + with self.assertRaises(RuntimeError) as ctx: + _ = server.target + self.assertIn("start()", str(ctx.exception)) + + def test_target_after_stop_raises(self): + """target accessed after stop() raises a clear error. + + Without resetting _port in stop(), target would keep pointing at a + defunct server, causing tests to fail with connection errors + instead of an obvious misuse signal. + """ + server = FakeGnoiServer() + server.start() + server.stop() + with self.assertRaises(RuntimeError): + _ = server.target + + def test_double_start_raises(self): + """start() on an already-started server raises rather than leaking + the first server's thread / listening socket.""" + server = FakeGnoiServer() + server.start() + try: + with self.assertRaises(RuntimeError) as ctx: + server.start() + self.assertIn("already-started", str(ctx.exception)) + finally: + server.stop() + + def test_stop_waits_for_termination(self): + """stop() returns only after the underlying gRPC server signals done. + + grpc.Server.stop(grace) returns a threading.Event; pinning the wait + prevents leaking server threads across many start/stop iterations. + """ + for _ in range(5): + server = FakeGnoiServer() + server.start() + target = server.target + server.stop() + # After stop returns, the port should no longer accept new + # connections — i.e., a new client should fail fast rather than + # hang. We don't strictly need to assert that here (would race + # with port reuse); the key contract is that stop() doesn't + # return mid-shutdown. Just exercising the loop without + # blowing the file-descriptor budget is the smoke test. + self.assertTrue(target.startswith("localhost:")) + + +class TestFakeSystemServicerEdgeCases(unittest.TestCase): + """Behavioral guarantees for FakeSystemServicer that aren't covered + elsewhere — sequence exhaustion fallback, explicit-empty-message + handling. + """ + + @classmethod + def setUpClass(cls): + cls.server = FakeGnoiServer() + cls.server.start() + + @classmethod + def tearDownClass(cls): + cls.server.stop() + + def setUp(self): + self.server.reset() + + def test_status_sequence_falls_back_to_single_response_after_exhaustion(self): + """Docstring promises: after sequence exhaustion, fall back to the + single configured response. Verify the contract.""" + single = system_pb2.RebootStatusResponse() + single.active = False + single.status.message = "fallback" + self.server.system.set_reboot_status(response=single) + + seq_item = system_pb2.RebootStatusResponse() + seq_item.active = True + seq_item.status.message = "from sequence" + self.server.system.set_reboot_status_sequence([seq_item]) + + with GnoiClient(self.server.target) as client: + r1 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + r2 = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + + self.assertEqual(r1.status.message, "from sequence") + self.assertTrue(r1.active) + # Second call: sequence exhausted, should return the single response + self.assertEqual(r2.status.message, "fallback") + self.assertFalse(r2.active) + + def test_set_reboot_status_message_can_be_cleared_to_empty(self): + """message='' should clear the previous value; previously the + falsy guard silently kept it.""" + self.server.system.set_reboot_status(message="hello") + self.server.system.set_reboot_status(message="") + + with GnoiClient(self.server.target) as client: + resp = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertEqual(resp.status.message, "") + + + def test_set_reboot_status_message_default_does_not_clobber(self): + """Default message=None must NOT overwrite a previously set message. + + Previously the default was ``message=""`` which the falsy guard + translated into a silent "keep previous"; changing the default to + ``None`` made the empty-string case meaningful but risks clobbering + on every other call if not handled correctly. Pin both halves of + the contract. + """ + self.server.system.set_reboot_status(message="orig") + # Call without message kwarg: message should be preserved. + self.server.system.set_reboot_status(active=True) + + with GnoiClient(self.server.target) as client: + resp = client.system.RebootStatus(system_pb2.RebootStatusRequest(), timeout=5) + self.assertEqual(resp.status.message, "orig") + self.assertTrue(resp.active) + + +if __name__ == '__main__': + unittest.main()