diff --git a/doc/gnoi-native-grpc-design.md b/doc/gnoi-native-grpc-design.md new file mode 100644 index 00000000..e534824a --- /dev/null +++ b/doc/gnoi-native-grpc-design.md @@ -0,0 +1,141 @@ +# Design: Replace `docker exec gnoi_client` with Native gRPC Calls + +## TL;DR + +`gnoi_shutdown_daemon` issues gNOI RPCs by shelling out to `docker exec gnmi gnoi_client`. This introduces several layers of indirection that make failures hard to diagnose and completion detection unreliable. This document proposes replacing the subprocess path with direct Python gRPC calls using vendored proto stubs. + +## 1. Limitations of the Current Approach + +| Observation | Consequence | +|-------------|-------------| +| Requires NPU `gnmi` container running and healthy | DPU shutdown depends on an unrelated NPU container's availability, even though the RPC target is the DPU's own gnmi server | +| Subprocess + Docker CLI overhead per RPC | Extra process creation, Docker round-trip, stdout capture on each call | +| Output is unstructured text with a header line | Parsing is coupled to `gnoi_client`'s print format, which has no stability guarantee | +| gRPC status codes are not propagated | Caller only sees `rc != 0` — no status code, no error details | +| Errors surface as Go `panic()` stack traces on stderr | Diagnosing RPC failures requires SSH + manual docker exec | +| `suppress_stderr=True` on the Reboot call | Panic output is discarded; logs show only "command failed" | +| Completion check uses string matching | `"reboot complete" in out_s.lower()` does not match actual output format (see §2) | +| Tight coupling to CLI flag interface | `-module System -rpc Reboot -jsonin '{...}'` adds a serialization layer between caller and protobuf | +| Broader privilege surface | Shell-out through Docker CLI vs. a direct gRPC socket | + +## 2. Existing Issue: RebootStatus Completion Detection + +The poll loop checks: + +```python +if rc_s == 0 and out_s and ("reboot complete" in out_s.lower()): + return True +``` + +Actual `gnoi_client` output ([source](https://github.com/sonic-net/sonic-gnmi/blob/master/gnoi_client/system/reboot.go)): + +``` +System RebootStatus +{"active":false,"status":{"status":"STATUS_SUCCESS","message":"..."}} +``` + +`"reboot complete"` does not appear in this output → the poll always exhausts its timeout regardless of DPU state. + +## 3. Proposed Change + +Replace subprocess calls with a thin Python gRPC client using vendored [gNOI System proto](https://github.com/openconfig/gnoi/blob/main/system/system.proto) stubs. + +**Before** (subprocess): +``` +docker exec gnmi gnoi_client -target=: -notls -module System -rpc Reboot -jsonin '{"method":3}' +``` + +**After** (direct gRPC): +```python +with GnoiClient(f"{dpu_ip}:{port}") as client: + client.reboot(method=REBOOT_METHOD_HALT, message="graceful shutdown") +``` + +For RebootStatus, check the protobuf response directly instead of string matching: +```python +resp = client.reboot_status() +if not resp.active and resp.status.status == STATUS_SUCCESS: + return True +``` + +### What this gives us + +**Better error detection** — gRPC errors carry status codes and details natively: +```python +except grpc.RpcError as e: + logger.log_error(f"{dpu_name}: Reboot failed: {e.code()} {e.details()}") + # e.g. "UNAVAILABLE: connection refused" vs today's "command failed" +``` + +**Better testing** — mocks operate on typed protobuf objects instead of crafting subprocess stdout strings: +```python +# Today: mock must reproduce gnoi_client's exact text output +mock_execute.return_value = (0, "reboot complete", "") # this doesn't even match reality + +# After: mock returns a typed response +mock_client.reboot_status.return_value = RebootStatusResponse( + active=False, status=RebootStatus(status=STATUS_SUCCESS)) +``` + +**Correct completion check** — inspect `resp.active` and `resp.status.status` directly instead of string matching. + +**Removes unnecessary NPU gnmi container dependency** — the current approach shells into the NPU's `gnmi` container to run `gnoi_client`, but there's no reason the NPU daemon needs the NPU gnmi container as an intermediary. The DPU's own gnmi server is the actual RPC endpoint; direct gRPC connects to it without involving the NPU container. + +**Scales to future RPCs** — the same pattern extends to any gNOI or gNMI call without adding more subprocess wrappers: +```python +# Adding a new gNOI RPC is just another method on the client +class GnoiClient: + def reboot(self, ...): ... + def reboot_status(self, ...): ... + def cancel_reboot(self, ...): ... # future + def system_time(self, ...): ... # future + +# Or a gNMI client alongside it +with GnmiClient(f"{dpu_ip}:{port}") as client: + client.get(path="/system/state/...") +``` +Each new RPC is a typed method with protobuf request/response — no new shell commands, no new output formats to parse. + +## 4. Scope + +### In scope +- Vendor Python gRPC stubs for `gnoi.system.System` (Reboot, RebootStatus) +- Lightweight `GnoiClient` wrapper +- Refactor the two RPC call sites in `GnoiRebootHandler` +- Update unit tests + +New directory structure: +``` +host_modules/gnoi/ +├── __init__.py +├── client.py # GnoiClient: reboot(), reboot_status(), context manager +├── system_pb2.py # vendored from openconfig/gnoi system.proto +├── system_pb2_grpc.py +├── types_pb2.py # dependency of system.proto +└── types_pb2_grpc.py +``` + +The daemon change is essentially replacing `execute_command(["docker", "exec", ...])` with: +```python +with GnoiClient(f"{dpu_ip}:{port}") as client: + client.reboot(method=REBOOT_METHOD_HALT, ...) + # ... + resp = client.reboot_status() +``` + +### Out of scope +- TLS/mTLS on midplane (future work; midplane is trusted today) +- Main loop, config DB subscription, halt flag handling — unchanged +- Other gNOI services beyond System + +## 5. Why Vendor Stubs? + +sonic-host-services has no proto compilation infra. The gNOI System proto is stable (no changes in years). Vendoring keeps the build simple; can migrate to build-time generation later if more protos are needed. + +## 6. Risks + +| Risk | Mitigation | +|------|------------| +| grpcio/protobuf not in host environment | Already used by other SONiC components | +| Proto drift from upstream gnoi | Pin to a specific commit; System service is stable | +| Insecure channel on midplane | Same trust model as today's `-notls`; TLS is future work | diff --git a/host_modules/gnoi/__init__.py b/host_modules/gnoi/__init__.py new file mode 100644 index 00000000..5990d743 --- /dev/null +++ b/host_modules/gnoi/__init__.py @@ -0,0 +1 @@ +# gNOI Python client - vendored proto stubs and client wrapper diff --git a/host_modules/gnoi/client.py b/host_modules/gnoi/client.py new file mode 100644 index 00000000..fb787055 --- /dev/null +++ b/host_modules/gnoi/client.py @@ -0,0 +1,48 @@ +""" +Lightweight Python gRPC client for gNOI services. + +Manages the gRPC channel and provides access to gNOI service stubs. +All RPCs are accessed through service properties, e.g.: + + with GnoiClient("10.0.0.1:8080") as client: + client.system.Reboot(request, timeout=60) + client.system.RebootStatus(request, timeout=10) +""" + +import grpc +from host_modules.gnoi import system_pb2_grpc + + +class GnoiClient: + """gNOI client managing a gRPC channel with access to service stubs.""" + + def __init__(self, target): + """ + Args: + target: gRPC target address, e.g. "10.0.0.1:8080" + """ + self._target = target + self._channel = None + + def __enter__(self): + self._channel = grpc.insecure_channel(self._target) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def close(self): + if self._channel: + self._channel.close() + self._channel = None + + @property + def channel(self): + """Access the underlying gRPC channel for custom stubs.""" + return self._channel + + @property + def system(self): + """gNOI System service stub (gnoi.system.System).""" + return system_pb2_grpc.SystemStub(self._channel) diff --git a/host_modules/gnoi/common_pb2.py b/host_modules/gnoi/common_pb2.py new file mode 100644 index 00000000..d5eda351 --- /dev/null +++ b/host_modules/gnoi/common_pb2.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: github.com/openconfig/gnoi/common/common.proto +# Protobuf Python Version: 6.31.1 +"""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, + 6, + 31, + 1, + '', + 'github.com/openconfig/gnoi/common/common.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from host_modules.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 not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_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/host_modules/gnoi/common_pb2_grpc.py b/host_modules/gnoi/common_pb2_grpc.py new file mode 100644 index 00000000..b2660b45 --- /dev/null +++ b/host_modules/gnoi/common_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.80.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},' + + ' but the generated code in github.com/openconfig/gnoi/common/common_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}.' + ) diff --git a/host_modules/gnoi/system_pb2.py b/host_modules/gnoi/system_pb2.py new file mode 100644 index 00000000..2be513bd --- /dev/null +++ b/host_modules/gnoi/system_pb2.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: github.com/openconfig/gnoi/system/system.proto +# Protobuf Python Version: 6.31.1 +"""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, + 6, + 31, + 1, + '', + 'github.com/openconfig/gnoi/system/system.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from host_modules.gnoi import common_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_common_dot_common__pb2 +from host_modules.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 not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z!github.com/openconfig/gnoi/system\322>\0051.4.0' + _globals['_TRACEROUTERESPONSE_MPLSENTRY']._loaded_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/host_modules/gnoi/system_pb2_grpc.py b/host_modules/gnoi/system_pb2_grpc.py new file mode 100644 index 00000000..f75f64fc --- /dev/null +++ b/host_modules/gnoi/system_pb2_grpc.py @@ -0,0 +1,480 @@ +# 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 host_modules.gnoi import system_pb2 as github_dot_com_dot_openconfig_dot_gnoi_dot_system_dot_system__pb2 + +GRPC_GENERATED_VERSION = '1.80.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},' + + ' but the generated code in github.com/openconfig/gnoi/system/system_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 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + 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, + _registered_method=True) + + +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,)) + server.add_registered_method_handlers('gnoi.system.System', rpc_method_handlers) + + + # 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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) + + @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, + _registered_method=True) diff --git a/host_modules/gnoi/types_pb2.py b/host_modules/gnoi/types_pb2.py new file mode 100644 index 00000000..549fd9bc --- /dev/null +++ b/host_modules/gnoi/types_pb2.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: github.com/openconfig/gnoi/types/types.proto +# Protobuf Python Version: 6.31.1 +"""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, + 6, + 31, + 1, + '', + 'github.com/openconfig/gnoi/types/types.proto' +) +# @@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 not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z github.com/openconfig/gnoi/types' + _globals['_PATHELEM_KEYENTRY']._loaded_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/host_modules/gnoi/types_pb2_grpc.py b/host_modules/gnoi/types_pb2_grpc.py new file mode 100644 index 00000000..5dce0282 --- /dev/null +++ b/host_modules/gnoi/types_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.80.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},' + + ' but the generated code in github.com/openconfig/gnoi/types/types_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}.' + ) diff --git a/scripts/gnoi_shutdown_daemon.py b/scripts/gnoi_shutdown_daemon.py index f20c72da..b9bfdd97 100644 --- a/scripts/gnoi_shutdown_daemon.py +++ b/scripts/gnoi_shutdown_daemon.py @@ -13,10 +13,13 @@ import os import redis import threading +import grpc import sonic_py_common.daemon_base as daemon_base from sonic_platform_base.module_base import ModuleBase from sonic_py_common import syslogger from swsscommon import swsscommon +from host_modules.gnoi.client import GnoiClient +from host_modules.gnoi import system_pb2 REBOOT_RPC_TIMEOUT_SEC = 60 # gNOI System.Reboot call timeout STATUS_POLL_TIMEOUT_SEC = 60 # overall time - polling RebootStatus @@ -235,36 +238,44 @@ def _wait_for_gnoi_halt_in_progress(self, dpu_name: str) -> bool: return False def _send_reboot_command(self, dpu_name: str, dpu_ip: str, port: str) -> bool: - """Send gNOI Reboot HALT command to the DPU.""" - reboot_cmd = [ - "docker", "exec", "gnmi", "gnoi_client", - f"-target={dpu_ip}:{port}", - "-logtostderr", "-notls", - "-module", "System", - "-rpc", "Reboot", - "-jsonin", json.dumps({"method": REBOOT_METHOD_HALT, "message": "Triggered by SmartSwitch graceful shutdown"}) - ] - rc, out, err = execute_command(reboot_cmd, timeout_sec=REBOOT_RPC_TIMEOUT_SEC, suppress_stderr=True) - if rc != 0: - logger.log_error(f"{dpu_name}: Reboot command failed") + """Send gNOI Reboot HALT command to the DPU via direct gRPC.""" + try: + with GnoiClient(f"{dpu_ip}:{port}") as client: + request = system_pb2.RebootRequest( + method=system_pb2.HALT, + message="Triggered by SmartSwitch graceful shutdown", + ) + client.system.Reboot(request, timeout=REBOOT_RPC_TIMEOUT_SEC) + return True + except grpc.RpcError as e: + logger.log_error(f"{dpu_name}: Reboot RPC failed: {e.code()} {e.details()}") + return False + except Exception as e: + logger.log_error(f"{dpu_name}: Reboot command failed: {e}") return False - return True def _poll_reboot_status(self, dpu_name: str, dpu_ip: str, port: str) -> bool: - """Poll RebootStatus until completion or timeout.""" + """Poll RebootStatus via direct gRPC until completion or timeout.""" deadline = time.monotonic() + _get_halt_timeout() - status_cmd = [ - "docker", "exec", "gnmi", "gnoi_client", - f"-target={dpu_ip}:{port}", - "-logtostderr", "-notls", - "-module", "System", - "-rpc", "RebootStatus" - ] - while time.monotonic() < deadline: - rc_s, out_s, err_s = execute_command(status_cmd, timeout_sec=STATUS_RPC_TIMEOUT_SEC) - if rc_s == 0 and out_s and ("reboot complete" in out_s.lower()): - return True - time.sleep(STATUS_POLL_INTERVAL_SEC) + try: + with GnoiClient(f"{dpu_ip}:{port}") as client: + while time.monotonic() < deadline: + try: + request = system_pb2.RebootStatusRequest() + resp = client.system.RebootStatus(request, timeout=STATUS_RPC_TIMEOUT_SEC) + if not resp.active and resp.status.status == system_pb2.RebootStatus.Status.STATUS_SUCCESS: + return True + if not resp.active and resp.status.status != system_pb2.RebootStatus.Status.STATUS_SUCCESS: + logger.log_error( + f"{dpu_name}: RebootStatus reports failure: " + f"status={resp.status.status}, message={resp.status.message}" + ) + return False + except grpc.RpcError as e: + logger.log_warning(f"{dpu_name}: RebootStatus poll RPC error: {e.code()} {e.details()}") + time.sleep(STATUS_POLL_INTERVAL_SEC) + except Exception as e: + logger.log_error(f"{dpu_name}: RebootStatus polling failed: {e}") logger.log_notice(f"{dpu_name}: Timeout waiting for RebootStatus completion, proceeding with halt flag clear") return False diff --git a/setup.py b/setup.py index 0b7252ed..b6155010 100644 --- a/setup.py +++ b/setup.py @@ -31,11 +31,13 @@ maintainer_email = 'jolevequ@microsoft.com', packages = [ 'host_modules', + 'host_modules.gnoi', 'utils' ], # Map packages to their actual dirs package_dir = { 'host_modules': 'host_modules', + 'host_modules.gnoi': 'host_modules/gnoi', 'utils': 'utils' }, scripts=[ @@ -58,7 +60,9 @@ 'Jinja2>=2.10', 'PyGObject', 'pycairo==1.26.1', - 'psutil' + 'psutil', + 'grpcio', + 'protobuf' ] + sonic_dependencies, setup_requires = [ 'pytest-runner', diff --git a/tests/gnoi_shutdown_daemon_test.py b/tests/gnoi_shutdown_daemon_test.py index bd1395f6..03a10b34 100644 --- a/tests/gnoi_shutdown_daemon_test.py +++ b/tests/gnoi_shutdown_daemon_test.py @@ -5,13 +5,56 @@ import os import json -# Mock redis module (available in SONiC runtime, not in test environment) +# Mock SONiC and system modules not available in test environment sys.modules['redis'] = MagicMock() +sys.modules['sonic_py_common'] = MagicMock() +sys.modules['sonic_py_common.daemon_base'] = MagicMock() +sys.modules['sonic_py_common.syslogger'] = MagicMock() +sys.modules['swsscommon'] = MagicMock() +sys.modules['swsscommon.swsscommon'] = MagicMock() + +# Mock grpc before importing daemon +mock_grpc = MagicMock() +mock_grpc.RpcError = type('RpcError', (Exception,), {}) +sys.modules['grpc'] = mock_grpc + +# Mock the gnoi proto stubs +mock_system_pb2 = MagicMock() +mock_system_pb2.HALT = 3 +mock_system_pb2.RebootRequest = MagicMock() +mock_system_pb2.RebootStatusRequest = MagicMock() +mock_status_enum = MagicMock() +mock_status_enum.STATUS_SUCCESS = 1 +mock_system_pb2.RebootStatus.Status = mock_status_enum + +mock_system_pb2_grpc = MagicMock() +sys.modules['host_modules'] = MagicMock() +sys.modules['host_modules.gnoi'] = MagicMock() +sys.modules['host_modules.gnoi.client'] = MagicMock() +sys.modules['host_modules.gnoi.system_pb2'] = mock_system_pb2 +sys.modules['host_modules.gnoi.system_pb2_grpc'] = mock_system_pb2_grpc + +# Mock sonic_platform_base for ModuleBase constants +mock_module_base = MagicMock() +mock_module_base.MODULE_STATUS_ONLINE = "Online" +mock_module_base.MODULE_STATUS_OFFLINE = "Offline" +mock_module_base.MODULE_STATUS_POWERED_DOWN = "PoweredDown" +mock_module_base.MODULE_STATUS_FAULT = "Fault" +mock_platform_base = MagicMock() +mock_platform_base.module_base.ModuleBase = mock_module_base +sys.modules['sonic_platform_base'] = mock_platform_base +sys.modules['sonic_platform_base.module_base'] = mock_platform_base.module_base sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'scripts'))) import gnoi_shutdown_daemon -from sonic_platform_base.module_base import ModuleBase + +# Use our mock ModuleBase +ModuleBase = mock_module_base + +# Patch the module-level references after import +gnoi_shutdown_daemon.grpc = mock_grpc +gnoi_shutdown_daemon.system_pb2 = mock_system_pb2 # Common fixtures mock_message = { @@ -19,86 +62,80 @@ "channel": f"__keyspace@{gnoi_shutdown_daemon.CONFIG_DB_INDEX}__:CHASSIS_MODULE|DPU0", "data": "hset", } -mock_config_entry = { - "admin_status": "down" -} -mock_ip_entry = {"ips": ["10.0.0.1"]} -mock_port_entry = {"gnmi_port": "12345"} + + +def _make_grpc_client_mock(reboot_status_resp=None, reboot_side_effect=None): + """Helper to create a mock GnoiClient context manager.""" + client = MagicMock() + client.__enter__ = MagicMock(return_value=client) + client.__exit__ = MagicMock(return_value=False) + # All service RPCs go through client.system.* + system_stub = MagicMock() + client.system = system_stub + if reboot_side_effect: + system_stub.Reboot.side_effect = reboot_side_effect + if reboot_status_resp is not None: + system_stub.RebootStatus.return_value = reboot_status_resp + return client class TestGnoiShutdownDaemon(unittest.TestCase): def setUp(self): - # Ensure a clean state for each test gnoi_shutdown_daemon.main = gnoi_shutdown_daemon.__dict__["main"] + # ---- execute_command (still used for non-gNOI commands) ---- + def test_execute_command_success(self): - """Test successful execution of a gNOI command.""" with patch("gnoi_shutdown_daemon.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0, stdout="success", stderr="") rc, stdout, stderr = gnoi_shutdown_daemon.execute_command(["dummy"]) self.assertEqual(rc, 0) self.assertEqual(stdout, "success") - self.assertEqual(stderr, "") def test_execute_command_timeout(self): - """Test gNOI command timeout.""" with patch("gnoi_shutdown_daemon.subprocess.run", side_effect=subprocess.TimeoutExpired(cmd=["dummy"], timeout=60)): rc, stdout, stderr = gnoi_shutdown_daemon.execute_command(["dummy"]) self.assertEqual(rc, -1) - self.assertEqual(stdout, "") self.assertIn("Command timed out", stderr) def test_execute_command_exception(self): - """Test gNOI command failure due to an exception.""" with patch("gnoi_shutdown_daemon.subprocess.run", side_effect=Exception("Test error")): rc, stdout, stderr = gnoi_shutdown_daemon.execute_command(["dummy"]) self.assertEqual(rc, -2) - self.assertEqual(stdout, "") self.assertIn("Command failed: Test error", stderr) + # ---- _get_halt_timeout ---- + def test_get_halt_timeout_from_platform_json(self): - """Test _get_halt_timeout with platform.json containing timeout.""" from unittest.mock import mock_open - mock_chassis = MagicMock() mock_chassis.get_name.return_value = "test_platform" - mock_platform_instance = MagicMock() mock_platform_instance.get_chassis.return_value = mock_chassis - mock_platform_class = MagicMock(return_value=mock_platform_instance) mock_platform_module = MagicMock() mock_platform_module.Platform = mock_platform_class - platform_json_content = {"dpu_halt_services_timeout": 120} - with patch.dict('sys.modules', {'sonic_platform': MagicMock(), 'sonic_platform.platform': mock_platform_module}): with patch("gnoi_shutdown_daemon.os.path.exists", return_value=True): - with patch("builtins.open", mock_open(read_data=json.dumps(platform_json_content))): - timeout = gnoi_shutdown_daemon._get_halt_timeout() - self.assertEqual(timeout, 120) + with patch("builtins.open", mock_open(read_data=json.dumps({"dpu_halt_services_timeout": 120}))): + self.assertEqual(gnoi_shutdown_daemon._get_halt_timeout(), 120) def test_get_halt_timeout_default(self): - """Test _get_halt_timeout returns default when platform.json not found.""" mock_chassis = MagicMock() mock_chassis.get_name.return_value = "test_platform" - mock_platform_instance = MagicMock() mock_platform_instance.get_chassis.return_value = mock_chassis - mock_platform_class = MagicMock(return_value=mock_platform_instance) mock_platform_module = MagicMock() mock_platform_module.Platform = mock_platform_class with patch.dict('sys.modules', {'sonic_platform': MagicMock(), 'sonic_platform.platform': mock_platform_module}): with patch("gnoi_shutdown_daemon.os.path.exists", return_value=False): - timeout = gnoi_shutdown_daemon._get_halt_timeout() - self.assertEqual(timeout, gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC) + self.assertEqual(gnoi_shutdown_daemon._get_halt_timeout(), gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC) def test_get_halt_timeout_exception(self): - """Test _get_halt_timeout returns default on exception.""" - # Mock sonic_platform import to succeed, then mock file operation to raise exception mock_chassis = MagicMock() mock_chassis.get_name.return_value = "test-platform" mock_platform_class = MagicMock() @@ -106,24 +143,20 @@ def test_get_halt_timeout_exception(self): with patch.dict('sys.modules', {'sonic_platform': MagicMock(), 'sonic_platform.platform': MagicMock(Platform=mock_platform_class)}), \ patch('gnoi_shutdown_daemon.open', side_effect=OSError("File system error")): - timeout = gnoi_shutdown_daemon._get_halt_timeout() - self.assertEqual(timeout, gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC) + self.assertEqual(gnoi_shutdown_daemon._get_halt_timeout(), gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC) + + # ---- Main loop ---- @patch('gnoi_shutdown_daemon.daemon_base.db_connect') @patch('gnoi_shutdown_daemon.GnoiRebootHandler') @patch('gnoi_shutdown_daemon.swsscommon.ConfigDBConnector') @patch('threading.Thread') def test_main_loop_flow(self, mock_thread, mock_config_db_connector_class, mock_gnoi_reboot_handler, mock_db_connect): - """Test the main loop processing of a shutdown event.""" - # Mock DB connections mock_state_db = MagicMock() mock_config_db = MagicMock() mock_db_connect.side_effect = [mock_state_db, mock_config_db] - - # Mock config_db.hget to return admin_status=down to trigger thread creation mock_config_db.hget.return_value = "down" - # Mock ConfigDBConnector for pubsub mock_config_db_connector = MagicMock() mock_config_db_connector.db_name = "CONFIG_DB" mock_pubsub = MagicMock() @@ -133,24 +166,14 @@ def test_main_loop_flow(self, mock_thread, mock_config_db_connector_class, mock_ mock_config_db_connector.get_redis_client.return_value = mock_redis_client mock_config_db_connector_class.return_value = mock_config_db_connector - # Mock chassis mock_chassis = MagicMock() mock_platform_instance = MagicMock() mock_platform_instance.get_chassis.return_value = mock_chassis - - # Create mock for sonic_platform.platform module mock_platform_submodule = MagicMock() mock_platform_submodule.Platform.return_value = mock_platform_instance - - # Create mock for sonic_platform parent module mock_sonic_platform = MagicMock() mock_sonic_platform.platform = mock_platform_submodule - # Mock the reboot handler's _handle_transition to avoid actual execution - mock_handler_instance = MagicMock() - mock_gnoi_reboot_handler.return_value = mock_handler_instance - - # Temporarily add mocks to sys.modules for the duration of this test with patch.dict('sys.modules', { 'sonic_platform': mock_sonic_platform, 'sonic_platform.platform': mock_platform_submodule @@ -158,208 +181,30 @@ def test_main_loop_flow(self, mock_thread, mock_config_db_connector_class, mock_ with self.assertRaises(KeyboardInterrupt): gnoi_shutdown_daemon.main() - # Verify initialization mock_db_connect.assert_has_calls([call("STATE_DB"), call("CONFIG_DB")]) - mock_gnoi_reboot_handler.assert_called_with(mock_state_db, mock_config_db, mock_chassis) - - # Verify that a thread was created to handle the transition mock_thread.assert_called_once() - # Verify the thread was started mock_thread.return_value.start.assert_called_once() - @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) - @patch('gnoi_shutdown_daemon.get_dpu_ip') - @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port') - @patch('gnoi_shutdown_daemon.execute_command') - @patch('gnoi_shutdown_daemon.time.sleep') - @patch('gnoi_shutdown_daemon.time.monotonic') - def test_handle_transition_success(self, mock_monotonic, mock_sleep, mock_execute_command, mock_get_gnmi_port, mock_get_dpu_ip, mock_get_halt_timeout): - """Test the full successful transition handling.""" - mock_db = MagicMock() - mock_config_db = MagicMock() - mock_chassis = MagicMock() - - # Mock return values - mock_get_dpu_ip.return_value = "10.0.0.1" - mock_get_gnmi_port.return_value = "8080" - - # Mock table.get() for gnoi_halt_in_progress check - mock_table = MagicMock() - mock_table.get.return_value = (True, [("gnoi_halt_in_progress", "True")]) - - # Mock time for polling - mock_monotonic.side_effect = [ - 0, 1, # For _wait_for_gnoi_halt_in_progress - 2, 3 # For _poll_reboot_status - ] - - # Reboot command success, RebootStatus success - mock_execute_command.side_effect = [ - (0, "reboot sent", ""), - (0, "reboot complete", "") - ] - - # Mock module for clear operation - mock_module = MagicMock() - mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE - mock_chassis.get_module_index.return_value = 0 - mock_chassis.get_module.return_value = mock_module - - with patch('gnoi_shutdown_daemon.swsscommon.Table', return_value=mock_table): - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) - result = handler._handle_transition("DPU0", "shutdown") - - self.assertTrue(result) - mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() - self.assertEqual(mock_execute_command.call_count, 2) - - @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) - @patch('gnoi_shutdown_daemon.get_dpu_ip') - @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port') - @patch('gnoi_shutdown_daemon.time.sleep') - @patch('gnoi_shutdown_daemon.time.monotonic') - @patch('gnoi_shutdown_daemon.execute_command') - def test_handle_transition_gnoi_halt_timeout(self, mock_execute_command, mock_monotonic, mock_sleep, mock_get_gnmi_port, mock_get_dpu_ip, mock_get_halt_timeout): - """Test transition proceeds despite gnoi_halt_in_progress timeout.""" - mock_db = MagicMock() - mock_config_db = MagicMock() - mock_chassis = MagicMock() - - mock_get_dpu_ip.return_value = "10.0.0.1" - mock_get_gnmi_port.return_value = "8080" - - # Mock table.get() to never return True (simulates timeout in wait) - mock_table = MagicMock() - mock_table.get.return_value = (True, [("gnoi_halt_in_progress", "False")]) - - # Simulate timeout in _wait_for_gnoi_halt_in_progress, then success in _poll_reboot_status - mock_monotonic.side_effect = [ - # _wait_for_gnoi_halt_in_progress times out - 0, 1, 2, gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC + 1, - # _poll_reboot_status succeeds - 0, 1 - ] - - # Reboot command and status succeed - mock_execute_command.side_effect = [ - (0, "reboot sent", ""), - (0, "reboot complete", "") - ] - - # Mock module for clear operation - mock_module = MagicMock() - mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE - mock_chassis.get_module_index.return_value = 0 - mock_chassis.get_module.return_value = mock_module - - with patch('gnoi_shutdown_daemon.swsscommon.Table', return_value=mock_table): - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) - result = handler._handle_transition("DPU0", "shutdown") - - # Should still succeed - code proceeds anyway after timeout warning - self.assertTrue(result) - mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() - - def test_get_dpu_ip_and_port(self): - """Test DPU IP and gNMI port retrieval.""" - # Test IP retrieval - mock_config = MagicMock() - mock_config.hget.return_value = "10.0.0.1" - - ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU0") - self.assertEqual(ip, "10.0.0.1") - mock_config.hget.assert_called_with("DHCP_SERVER_IPV4_PORT|bridge-midplane|dpu0", "ips@") - - # Test port retrieval - mock_config = MagicMock() - mock_config.hget.return_value = "12345" - - port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") - self.assertEqual(port, "12345") - - # Test port fallback - mock_config = MagicMock() - mock_config.hget.return_value = None - - port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") - self.assertEqual(port, "8080") - - @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) - @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value=None) - @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080") - def test_handle_transition_ip_failure(self, mock_get_gnmi_port, mock_get_dpu_ip, mock_get_halt_timeout): - """Test handle_transition failure on DPU IP retrieval.""" - mock_db = MagicMock() - mock_config_db = MagicMock() - mock_chassis = MagicMock() - - # Mock module for clear operation - mock_module = MagicMock() - mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE - mock_chassis.get_module_index.return_value = 0 - mock_chassis.get_module.return_value = mock_module - - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) - - # Mock _wait_for_gnoi_halt_in_progress to return immediately to prevent hanging - handler._wait_for_gnoi_halt_in_progress = MagicMock(return_value=True) - - result = handler._handle_transition("DPU0", "shutdown") - - self.assertFalse(result) - # Verify that clear_module_gnoi_halt_in_progress was called - mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() - - @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1") - @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080") - @patch('gnoi_shutdown_daemon.execute_command', return_value=(-1, "", "error")) - def test_send_reboot_command_failure(self, mock_execute, mock_get_port, mock_get_ip): - """Test failure of _send_reboot_command.""" - handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) - result = handler._send_reboot_command("DPU0", "10.0.0.1", "8080") - self.assertFalse(result) - - def test_get_dpu_gnmi_port_variants(self): - """Test DPU gNMI port retrieval with name variants.""" - mock_config = MagicMock() - mock_config.hget.side_effect = [ - None, # dpu0 fails - None, # DPU0 fails - "12345" # DPU0 succeeds - ] - - port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") - self.assertEqual(port, "12345") - self.assertEqual(mock_config.hget.call_count, 3) - @patch('gnoi_shutdown_daemon.daemon_base.db_connect') @patch('gnoi_shutdown_daemon.swsscommon.ConfigDBConnector') def test_main_loop_no_dpu_name(self, mock_config_db_connector_class, mock_db_connect): - """Test main loop with a malformed key.""" mock_chassis = MagicMock() mock_platform_instance = MagicMock() mock_platform_instance.get_chassis.return_value = mock_chassis - - # Create mock for sonic_platform.platform module mock_platform_submodule = MagicMock() mock_platform_submodule.Platform.return_value = mock_platform_instance - - # Create mock for sonic_platform parent module mock_sonic_platform = MagicMock() mock_sonic_platform.platform = mock_platform_submodule mock_pubsub = MagicMock() - # Malformed message, then stop malformed_message = mock_message.copy() malformed_message["channel"] = f"__keyspace@{gnoi_shutdown_daemon.CONFIG_DB_INDEX}__:CHASSIS_MODULE|" mock_pubsub.get_message.side_effect = [malformed_message, KeyboardInterrupt] - # Mock DB connections mock_state_db = MagicMock() mock_config_db = MagicMock() mock_db_connect.side_effect = [mock_state_db, mock_config_db] - # Mock ConfigDBConnector for pubsub mock_config_db_connector = MagicMock() mock_config_db_connector.db_name = "CONFIG_DB" mock_redis_client = MagicMock() @@ -377,29 +222,22 @@ def test_main_loop_no_dpu_name(self, mock_config_db_connector_class, mock_db_con @patch('gnoi_shutdown_daemon.daemon_base.db_connect') @patch('gnoi_shutdown_daemon.swsscommon.ConfigDBConnector') def test_main_loop_get_transition_exception(self, mock_config_db_connector_class, mock_db_connect): - """Test main loop when hget raises an exception.""" mock_chassis = MagicMock() mock_platform_instance = MagicMock() mock_platform_instance.get_chassis.return_value = mock_chassis - - # Create mock for sonic_platform.platform module mock_platform_submodule = MagicMock() mock_platform_submodule.Platform.return_value = mock_platform_instance - - # Create mock for sonic_platform parent module mock_sonic_platform = MagicMock() mock_sonic_platform.platform = mock_platform_submodule mock_pubsub = MagicMock() mock_pubsub.get_message.side_effect = [mock_message, KeyboardInterrupt] - # Mock config_db to raise exception on hget mock_config_db = MagicMock() mock_state_db = MagicMock() mock_db_connect.side_effect = [mock_state_db, mock_config_db] mock_config_db.hget.side_effect = AttributeError("DB error") - # Mock ConfigDBConnector for pubsub mock_config_db_connector = MagicMock() mock_config_db_connector.db_name = "CONFIG_DB" mock_redis_client = MagicMock() @@ -414,126 +252,270 @@ def test_main_loop_get_transition_exception(self, mock_config_db_connector_class with self.assertRaises(KeyboardInterrupt): gnoi_shutdown_daemon.main() - @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) - @patch('gnoi_shutdown_daemon.execute_command', return_value=(-1, "", "RPC error")) - def test_poll_reboot_status_failure(self, mock_execute_command, mock_get_halt_timeout): - """Test _poll_reboot_status with a command failure.""" - handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) - with patch('gnoi_shutdown_daemon.time.monotonic', side_effect=[0, 1, 61]): - result = handler._poll_reboot_status("DPU0", "10.0.0.1", "8080") - self.assertFalse(result) + # ---- DPU IP / Port helpers ---- - def test_sonic_platform_import_mock(self): - """Simple test to verify sonic_platform import mocking works.""" - # Create mock chassis - mock_chassis = MagicMock() - mock_chassis.get_name.return_value = "test_chassis" - - # Create mock platform instance that returns our chassis - mock_platform_instance = MagicMock() - mock_platform_instance.get_chassis.return_value = mock_chassis - - # Create mock Platform class - mock_platform_class = MagicMock(return_value=mock_platform_instance) - - # Create mock for sonic_platform.platform module - mock_platform_submodule = MagicMock() - mock_platform_submodule.Platform = mock_platform_class - - # Create mock for sonic_platform parent module - mock_sonic_platform = MagicMock() - mock_sonic_platform.platform = mock_platform_submodule + def test_get_dpu_ip_and_port(self): + mock_config = MagicMock() + mock_config.hget.return_value = "10.0.0.1" + ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU0") + self.assertEqual(ip, "10.0.0.1") + mock_config.hget.assert_called_with("DHCP_SERVER_IPV4_PORT|bridge-midplane|dpu0", "ips@") - # Test that we can mock the import - with patch.dict('sys.modules', { - 'sonic_platform': mock_sonic_platform, - 'sonic_platform.platform': mock_platform_submodule - }): - # Simulate what the actual code does - from sonic_platform import platform - chassis = platform.Platform().get_chassis() + mock_config = MagicMock() + mock_config.hget.return_value = "12345" + port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") + self.assertEqual(port, "12345") - # Verify it worked - self.assertEqual(chassis, mock_chassis) - self.assertEqual(chassis.get_name(), "test_chassis") - mock_platform_class.assert_called_once() - mock_platform_instance.get_chassis.assert_called_once() + mock_config = MagicMock() + mock_config.hget.return_value = None + port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") + self.assertEqual(port, "8080") def test_get_dpu_ip_with_string_ips(self): - """Test get_dpu_ip when ips is a string instead of list.""" mock_config = MagicMock() mock_config.hget.return_value = "10.0.0.5" - - ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1") - self.assertEqual(ip, "10.0.0.5") + self.assertEqual(gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1"), "10.0.0.5") def test_get_dpu_ip_empty_entry(self): - """Test get_dpu_ip when entry is empty.""" mock_config = MagicMock() mock_config.hget.return_value = None - - ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1") - self.assertIsNone(ip) + self.assertIsNone(gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1")) def test_get_dpu_ip_no_ips_field(self): - """Test get_dpu_ip when hget returns None (field doesn't exist).""" mock_config = MagicMock() mock_config.hget.return_value = None - - ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1") - self.assertIsNone(ip) + self.assertIsNone(gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1")) def test_get_dpu_ip_exception(self): - """Test get_dpu_ip when exception occurs.""" mock_config = MagicMock() mock_config.hget.side_effect = AttributeError("Database error") - - ip = gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1") - self.assertIsNone(ip) + self.assertIsNone(gnoi_shutdown_daemon.get_dpu_ip(mock_config, "DPU1")) def test_get_dpu_gnmi_port_exception(self): - """Test get_dpu_gnmi_port when exception occurs.""" mock_config = MagicMock() mock_config.hget.side_effect = AttributeError("Database error") + self.assertEqual(gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU1"), "8080") - port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU1") - self.assertEqual(port, "8080") + def test_get_dpu_gnmi_port_variants(self): + mock_config = MagicMock() + mock_config.hget.side_effect = [None, None, "12345"] + port = gnoi_shutdown_daemon.get_dpu_gnmi_port(mock_config, "DPU0") + self.assertEqual(port, "12345") + self.assertEqual(mock_config.hget.call_count, 3) + + # ---- gRPC: _send_reboot_command ---- + + @patch('gnoi_shutdown_daemon.GnoiClient') + def test_send_reboot_command_success(self, mock_gnoi_client_class): + mock_client = _make_grpc_client_mock() + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertTrue(handler._send_reboot_command("DPU0", "10.0.0.1", "8080")) + mock_client.system.Reboot.assert_called_once() + + @patch('gnoi_shutdown_daemon.GnoiClient') + def test_send_reboot_command_grpc_error(self, mock_gnoi_client_class): + rpc_error = mock_grpc.RpcError() + rpc_error.code = MagicMock(return_value="UNAVAILABLE") + rpc_error.details = MagicMock(return_value="connection refused") - def test_send_reboot_command_success(self): - """Test successful _send_reboot_command.""" - with patch('gnoi_shutdown_daemon.execute_command', return_value=(0, "success", "")): - handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) - result = handler._send_reboot_command("DPU0", "10.0.0.1", "8080") - self.assertTrue(result) + mock_client = _make_grpc_client_mock(reboot_side_effect=rpc_error) + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertFalse(handler._send_reboot_command("DPU0", "10.0.0.1", "8080")) + + # ---- gRPC: _poll_reboot_status ---- @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) - @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1") - @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', side_effect=Exception("Port lookup failed")) - def test_handle_transition_config_exception(self, mock_get_port, mock_get_ip, mock_get_halt_timeout): - """Test handle_transition when configuration lookup raises exception.""" + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_poll_reboot_status_success(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, _): + mock_monotonic.side_effect = [0, 1] + mock_resp = MagicMock() + mock_resp.active = False + mock_resp.status.status = mock_status_enum.STATUS_SUCCESS + + mock_client = _make_grpc_client_mock(reboot_status_resp=mock_resp) + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertTrue(handler._poll_reboot_status("DPU0", "10.0.0.1", "8080")) + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_poll_reboot_status_timeout(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, _): + mock_monotonic.side_effect = [0, 1, 61] + mock_resp = MagicMock() + mock_resp.active = True + + mock_client = _make_grpc_client_mock(reboot_status_resp=mock_resp) + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertFalse(handler._poll_reboot_status("DPU0", "10.0.0.1", "8080")) + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_poll_reboot_status_failure_status(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, _): + mock_monotonic.side_effect = [0, 1] + mock_resp = MagicMock() + mock_resp.active = False + mock_resp.status.status = 2 # not STATUS_SUCCESS + mock_resp.status.message = "internal error" + + mock_client = _make_grpc_client_mock(reboot_status_resp=mock_resp) + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertFalse(handler._poll_reboot_status("DPU0", "10.0.0.1", "8080")) + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_poll_reboot_status_rpc_error_recovery(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, _): + mock_monotonic.side_effect = [0, 1, 2] + + rpc_error = mock_grpc.RpcError() + rpc_error.code = MagicMock(return_value="UNAVAILABLE") + rpc_error.details = MagicMock(return_value="transient") + + mock_resp = MagicMock() + mock_resp.active = False + mock_resp.status.status = mock_status_enum.STATUS_SUCCESS + + mock_client = _make_grpc_client_mock() + mock_client.system.RebootStatus.side_effect = [rpc_error, mock_resp] + mock_gnoi_client_class.return_value = mock_client + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), MagicMock()) + self.assertTrue(handler._poll_reboot_status("DPU0", "10.0.0.1", "8080")) + + # ---- _handle_transition ---- + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.get_dpu_ip') + @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port') + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_handle_transition_success(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, mock_get_gnmi_port, mock_get_dpu_ip, _): + mock_db = MagicMock() + mock_config_db = MagicMock() + mock_chassis = MagicMock() + + mock_get_dpu_ip.return_value = "10.0.0.1" + mock_get_gnmi_port.return_value = "8080" + + mock_table = MagicMock() + mock_table.get.return_value = (True, [("gnoi_halt_in_progress", "True")]) + + mock_monotonic.side_effect = [0, 1, 2, 3] + + mock_reboot_client = _make_grpc_client_mock() + mock_status_resp = MagicMock() + mock_status_resp.active = False + mock_status_resp.status.status = mock_status_enum.STATUS_SUCCESS + mock_status_client = _make_grpc_client_mock(reboot_status_resp=mock_status_resp) + mock_gnoi_client_class.side_effect = [mock_reboot_client, mock_status_client] + + mock_module = MagicMock() + mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE + mock_chassis.get_module_index.return_value = 0 + mock_chassis.get_module.return_value = mock_module + + with patch('gnoi_shutdown_daemon.swsscommon.Table', return_value=mock_table): + handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + result = handler._handle_transition("DPU0", "shutdown") + + self.assertTrue(result) + mock_reboot_client.system.Reboot.assert_called_once() + mock_status_client.system.RebootStatus.assert_called_once() + mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.get_dpu_ip') + @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port') + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_handle_transition_gnoi_halt_timeout(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, mock_get_gnmi_port, mock_get_dpu_ip, _): mock_db = MagicMock() mock_config_db = MagicMock() mock_chassis = MagicMock() - # Mock module for clear operation + mock_get_dpu_ip.return_value = "10.0.0.1" + mock_get_gnmi_port.return_value = "8080" + + mock_table = MagicMock() + mock_table.get.return_value = (True, [("gnoi_halt_in_progress", "False")]) + + mock_monotonic.side_effect = [ + 0, 1, 2, gnoi_shutdown_daemon.STATUS_POLL_TIMEOUT_SEC + 1, + 0, 1 + ] + + mock_reboot_client = _make_grpc_client_mock() + mock_status_resp = MagicMock() + mock_status_resp.active = False + mock_status_resp.status.status = mock_status_enum.STATUS_SUCCESS + mock_status_client = _make_grpc_client_mock(reboot_status_resp=mock_status_resp) + mock_gnoi_client_class.side_effect = [mock_reboot_client, mock_status_client] + mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = mock_module - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + with patch('gnoi_shutdown_daemon.swsscommon.Table', return_value=mock_table): + handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + result = handler._handle_transition("DPU0", "shutdown") - # Mock _wait_for_gnoi_halt_in_progress to return immediately to prevent hanging + self.assertTrue(result) + mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() + + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value=None) + @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080") + def test_handle_transition_ip_failure(self, mock_get_gnmi_port, mock_get_dpu_ip, _): + mock_chassis = MagicMock() + mock_module = MagicMock() + mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE + mock_chassis.get_module_index.return_value = 0 + mock_chassis.get_module.return_value = mock_module + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) handler._wait_for_gnoi_halt_in_progress = MagicMock(return_value=True) - result = handler._handle_transition("DPU0", "shutdown") + self.assertFalse(handler._handle_transition("DPU0", "shutdown")) + mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() - self.assertFalse(result) - # Verify that clear_module_gnoi_halt_in_progress was called + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1") + @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', side_effect=Exception("Port lookup failed")) + def test_handle_transition_config_exception(self, mock_get_port, mock_get_ip, _): + mock_chassis = MagicMock() + mock_module = MagicMock() + mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE + mock_chassis.get_module_index.return_value = 0 + mock_chassis.get_module.return_value = mock_module + + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) + handler._wait_for_gnoi_halt_in_progress = MagicMock(return_value=True) + + self.assertFalse(handler._handle_transition("DPU0", "shutdown")) mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() + # ---- _should_skip_gnoi_shutdown (from upstream #352) ---- + def test_should_skip_gnoi_shutdown_offline(self): - """Test _should_skip_gnoi_shutdown returns True for Offline DPU.""" mock_chassis = MagicMock() mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_OFFLINE @@ -544,7 +526,6 @@ def test_should_skip_gnoi_shutdown_offline(self): self.assertTrue(handler._should_skip_gnoi_shutdown("DPU0")) def test_should_skip_gnoi_shutdown_powered_down(self): - """Test _should_skip_gnoi_shutdown returns True for PoweredDown DPU.""" mock_chassis = MagicMock() mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_POWERED_DOWN @@ -555,7 +536,6 @@ def test_should_skip_gnoi_shutdown_powered_down(self): self.assertTrue(handler._should_skip_gnoi_shutdown("DPU0")) def test_should_skip_gnoi_shutdown_online(self): - """Test _should_skip_gnoi_shutdown returns False for Online DPU.""" mock_chassis = MagicMock() mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_ONLINE @@ -566,7 +546,6 @@ def test_should_skip_gnoi_shutdown_online(self): self.assertFalse(handler._should_skip_gnoi_shutdown("DPU0")) def test_should_skip_gnoi_shutdown_fault(self): - """Test _should_skip_gnoi_shutdown returns False for Fault DPU.""" mock_chassis = MagicMock() mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_FAULT @@ -577,7 +556,6 @@ def test_should_skip_gnoi_shutdown_fault(self): self.assertFalse(handler._should_skip_gnoi_shutdown("DPU0")) def test_should_skip_gnoi_shutdown_bad_index(self): - """Test _should_skip_gnoi_shutdown returns None when module index is negative.""" mock_chassis = MagicMock() mock_chassis.get_module_index.return_value = -1 @@ -585,7 +563,6 @@ def test_should_skip_gnoi_shutdown_bad_index(self): self.assertIsNone(handler._should_skip_gnoi_shutdown("DPU0")) def test_should_skip_gnoi_shutdown_no_module(self): - """Test _should_skip_gnoi_shutdown returns None when module is None.""" mock_chassis = MagicMock() mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = None @@ -593,122 +570,94 @@ def test_should_skip_gnoi_shutdown_no_module(self): handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) self.assertIsNone(handler._should_skip_gnoi_shutdown("DPU0")) + # ---- _handle_transition with oper_status skip (from upstream #352) ---- + def test_handle_transition_dpu_already_offline(self): - """Test that gNOI shutdown is skipped when DPU is already offline.""" - mock_db = MagicMock() - mock_config_db = MagicMock() mock_chassis = MagicMock() - - # Mock module with Offline oper_status mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_OFFLINE mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = mock_module - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) result = handler._handle_transition("DPU0", "shutdown") - - # Should return True (success) without attempting gNOI reboot self.assertTrue(result) - mock_module.get_oper_status.assert_called_once() mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() def test_handle_transition_dpu_powered_down(self): - """Test that gNOI shutdown is skipped when DPU is in PoweredDown state.""" - mock_db = MagicMock() - mock_config_db = MagicMock() mock_chassis = MagicMock() - - # Mock module with PoweredDown oper_status mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_POWERED_DOWN mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = mock_module - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) result = handler._handle_transition("DPU0", "shutdown") - - # Should return True (success) without attempting gNOI reboot self.assertTrue(result) - mock_module.get_oper_status.assert_called_once() mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() - def test_handle_transition_dpu_fault_proceeds(self): - """Test that gNOI shutdown proceeds when DPU is in Fault state.""" - mock_db = MagicMock() - mock_config_db = MagicMock() + @patch('gnoi_shutdown_daemon._get_halt_timeout', return_value=60) + @patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1") + @patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080") + @patch('gnoi_shutdown_daemon.GnoiClient') + @patch('gnoi_shutdown_daemon.time.sleep') + @patch('gnoi_shutdown_daemon.time.monotonic') + def test_handle_transition_dpu_fault_proceeds(self, mock_monotonic, mock_sleep, mock_gnoi_client_class, mock_port, mock_ip, _): + """DPU in Fault state should still attempt gNOI shutdown.""" mock_chassis = MagicMock() - - # Mock module with Fault oper_status — should NOT skip mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_FAULT mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = mock_module - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) + mock_table = MagicMock() + mock_table.get.return_value = (True, [("gnoi_halt_in_progress", "True")]) - # Mock remaining methods to prevent actual gNOI calls - handler._wait_for_gnoi_halt_in_progress = MagicMock(return_value=True) - handler._send_reboot_command = MagicMock(return_value=True) - handler._poll_reboot_status = MagicMock(return_value=True) - handler._clear_halt_flag = MagicMock(return_value=True) + mock_monotonic.side_effect = [0, 1, 2, 3] + + mock_reboot_client = _make_grpc_client_mock() + mock_status_resp = MagicMock() + mock_status_resp.active = False + mock_status_resp.status.status = mock_status_enum.STATUS_SUCCESS + mock_status_client = _make_grpc_client_mock(reboot_status_resp=mock_status_resp) + mock_gnoi_client_class.side_effect = [mock_reboot_client, mock_status_client] - with patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1"), \ - patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080"): + with patch('gnoi_shutdown_daemon.swsscommon.Table', return_value=mock_table): + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) result = handler._handle_transition("DPU0", "shutdown") - # Should proceed with shutdown for Fault state self.assertTrue(result) - handler._wait_for_gnoi_halt_in_progress.assert_called_once() - handler._send_reboot_command.assert_called_once() + mock_module.clear_module_gnoi_halt_in_progress.assert_called_once() def test_handle_transition_dpu_offline_clear_halt_failure(self): - """Test that _clear_halt_flag failure is propagated when DPU is offline.""" - mock_db = MagicMock() - mock_config_db = MagicMock() + """Test clear_halt_flag failure path when DPU is already offline.""" mock_chassis = MagicMock() - - # Mock module with Offline oper_status mock_module = MagicMock() mock_module.get_oper_status.return_value = ModuleBase.MODULE_STATUS_OFFLINE + mock_module.clear_module_gnoi_halt_in_progress.side_effect = Exception("platform error") mock_chassis.get_module_index.return_value = 0 mock_chassis.get_module.return_value = mock_module - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) - # Make _clear_halt_flag fail - handler._clear_halt_flag = MagicMock(return_value=False) - + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) result = handler._handle_transition("DPU0", "shutdown") - - # Should return False since _clear_halt_flag failed + # When skip=True but clear_halt_flag fails, returns False (= cleared failed) self.assertFalse(result) - handler._clear_halt_flag.assert_called_once_with("DPU0") def test_handle_transition_oper_status_check_exception(self): - """Test that gNOI shutdown proceeds when oper_status check raises exception.""" - mock_db = MagicMock() - mock_config_db = MagicMock() + """Test that transition proceeds when oper status check raises exception.""" mock_chassis = MagicMock() + mock_chassis.get_module_index.side_effect = Exception("chassis error") - # Mock module to raise exception on get_module_index - mock_chassis.get_module_index.side_effect = Exception("Platform error") - - handler = gnoi_shutdown_daemon.GnoiRebootHandler(mock_db, mock_config_db, mock_chassis) - - # Mock remaining methods to prevent actual gNOI calls + handler = gnoi_shutdown_daemon.GnoiRebootHandler(MagicMock(), MagicMock(), mock_chassis) + # Mock remaining methods to prevent actual execution handler._wait_for_gnoi_halt_in_progress = MagicMock(return_value=True) - handler._send_reboot_command = MagicMock(return_value=True) - handler._poll_reboot_status = MagicMock(return_value=True) + handler._send_reboot_command = MagicMock(return_value=False) handler._clear_halt_flag = MagicMock(return_value=True) - with patch('gnoi_shutdown_daemon.get_dpu_ip', return_value="10.0.0.1"), \ - patch('gnoi_shutdown_daemon.get_dpu_gnmi_port', return_value="8080"): - result = handler._handle_transition("DPU0", "shutdown") - - # Should proceed with shutdown despite oper_status check failure - self.assertTrue(result) - handler._wait_for_gnoi_halt_in_progress.assert_called_once() - handler._send_reboot_command.assert_called_once() + # Should proceed with transition despite oper check failure + result = handler._handle_transition("DPU0", "shutdown") + # Transition fails because send_reboot returns False + self.assertFalse(result) if __name__ == '__main__':