plesty.server.services.nodes ============================ .. py:module:: plesty.server.services.nodes .. autoapi-nested-parse:: Reaching the nodes a master drives. The console has always managed the bench it runs on. A master (:mod:`plesty.server.model.agent`) also shows the devices of the benches it declares as peers, and acts on them — so it needs, for the first time on this side, a client of another bench. That client already exists and is already what ``plesty-bench`` uses: :class:`plesty.bench.BenchClient`. This module is the thin layer between it and the presenter. **One link per node, kept open.** A poll every three seconds is not worth a connection each; the link reconnects on its own when a request times out. **A link is called from a thread pool**, so every request holds the link's lock: a ZMQ socket belongs to one thread at a time, and the window's workers make no promise about which of them asks. **A node that does not answer is a state, not a failure.** It keeps its row, its devices go unreachable, and :attr:`NodeLink.trouble` says why — a console that dropped the rows of a bench somebody rebooted would be reporting that the devices are gone. Attributes ---------- .. autoapisummary:: plesty.server.services.nodes.POLL_TIMEOUT_MS plesty.server.services.nodes.ACTION_TIMEOUT_MS plesty.server.services.nodes.Connect Exceptions ---------- .. autoapisummary:: plesty.server.services.nodes.NodeError Classes ------- .. autoapisummary:: plesty.server.services.nodes.NodeLink plesty.server.services.nodes.NodeDevice plesty.server.services.nodes.Nodes Functions --------- .. autoapisummary:: plesty.server.services.nodes._connect Module Contents --------------- .. py:data:: POLL_TIMEOUT_MS :value: 3000 .. py:data:: ACTION_TIMEOUT_MS :value: 120000 .. py:data:: Connect .. py:exception:: NodeError Bases: :py:obj:`RuntimeError` A node could not be reached, or refused what it was asked. Initialize self. See help(type(self)) for accurate signature. .. py:function:: _connect(peer: plesty.server.model.Peer) -> plesty.bench.BenchClient .. py:class:: NodeLink(peer: plesty.server.model.Peer, connect: Connect | None = None) This master's link to one node's agent. Bind to *peer*; the socket opens on the first request. :param peer: The node's name, address and secret. :param connect: Builds the client; injected by the tests. .. py:attribute:: peer .. py:attribute:: _connect .. py:attribute:: _client :type: plesty.bench.BenchClient | None :value: None .. py:attribute:: _lock .. py:attribute:: _trouble :type: str :value: '' .. py:property:: name :type: str The node's name — what the Host column shows. .. py:property:: trouble :type: str Why the last request failed, or empty after one that worked. .. py:property:: reachable :type: bool Whether the last request got an answer. .. py:method:: close() -> None Drop the connection; a later request opens a new one. .. py:method:: run(argv: collections.abc.Sequence[str], timeout_ms: int = ACTION_TIMEOUT_MS) -> str Run ``plesty-server `` on the node and return its output. :param argv: The command line, as the local CLI would take it. :param timeout_ms: Reply timeout. :returns: Standard output, stripped. :raises NodeError: When the node did not answer, or the command failed — carrying what the node said, since it is the node that knows. .. py:method:: devices() -> list[dict[str, Any]] Every device the node declares, as ``list --json`` reports them. The same document the local ``list --json`` prints, so one call carries the declaration, the installed version, the source line and the state — everything a row needs, in one round trip per node per refresh. :raises NodeError: When the node did not answer or gave no JSON. .. py:method:: device(name: str) -> NodeDevice A handle on one running device of the node, for a control panel. On a connection of its own, **not** the one this link polls with. A panel drives its handle from its own worker thread while the device table keeps polling on ours, and a ZMQ socket belongs to one thread at a time: sharing one would let a panel's reply answer a poll, and every request after that would return the one before it. .. py:class:: NodeDevice(peer: plesty.server.model.Peer, name: str, connect: Connect) One device of a node, on a connection this handle owns. What a control panel drives. It is the four calls of plesty-lib's ``DeviceHandle`` — ``describe``, ``query``, ``write``, ``call`` — delegated to a :class:`plesty.bench.RemoteDevice`, plus the one thing that handle cannot offer: a ``close`` that actually releases something. :class:`plesty.bench.RemoteDevice` closes nothing by design, because its connection belongs to the ``BenchClient`` that made it. A panel needs the opposite — a connection of its own, released when the panel goes — so that the console's poll and the panel never share a socket, and so that the console's ``_release`` has something to close. The client connects lazily, so the socket is made by whichever thread makes the first call. That is the panel's worker, which is the thread that must own it. Open a handle on *name* at *peer*. :param peer: The node the device runs on. :param name: The declared device. :param connect: Builds the client; injected by the tests. .. py:attribute:: name .. py:attribute:: peer .. py:attribute:: _client .. py:attribute:: _device :type: Any .. py:method:: describe() -> dict[str, Any] The device's methods and its serialized doc model. .. py:method:: query(key: str) -> Any Read configuration parameter *key*. .. py:method:: write(key: str, value: Any) -> Any Write *value* to configuration parameter *key*. .. py:method:: call(op: str, **kwargs: Any) -> Any Run operation *op*. .. py:method:: close() -> None Release this handle's own connection. .. py:class:: Nodes(peers: collections.abc.Iterable[plesty.server.model.Peer] = (), connect: Connect | None = None) Every node a master drives, one :class:`NodeLink` each. Hold a link per peer. :param peers: The declared nodes. :param connect: Builds each client; injected by the tests. .. py:attribute:: _connect :value: None .. py:attribute:: _links :type: dict[str, NodeLink] .. py:method:: refresh(peers: collections.abc.Iterable[plesty.server.model.Peer]) -> None Make the links match *peers*, keeping the connections still wanted. A peer whose address or secret changed is reconnected; one that is simply still there keeps the socket it has, so editing the list does not cost every other node its connection. .. py:property:: links :type: list[NodeLink] The links, in declaration order. .. py:property:: names :type: list[str] The node names. .. py:method:: link(name: str) -> NodeLink The link to the node *name*. :raises NodeError: When no such node is declared. .. py:method:: close() -> None Drop every connection.