diff --git a/doc/man/exporter.rst b/doc/man/exporter.rst index c30ca3180..1a0f581af 100644 --- a/doc/man/exporter.rst +++ b/doc/man/exporter.rst @@ -77,13 +77,38 @@ for more information. ENVIRONMENT VARIABLES --------------------- -The following environment variable can be used to configure labgrid-exporter. +The following environment variables can be used to configure +labgrid-exporter. LG_COORDINATOR ~~~~~~~~~~~~~~ This variable can be used to set the default coordinator in the format ``HOST[:PORT]`` (instead of using the ``-x`` option). +LG_SERIAL_TRACE_DIR +~~~~~~~~~~~~~~~~~~~ +When set, the exporter records all serial-port traffic for each +acquired resource into ``/-.log``, +where ```` is the resource group name and ```` is the +acquiring user identity reported by the coordinator (in +``host/user`` form, with ``/`` rewritten to ``_``). Both directions +are captured verbatim, and ser2net writes timestamped OPEN/CLOSE +markers at the start and end of each session so the lab admin has +an independent record that is not affected by per-client logging +options. + +The directory is created on demand. A fresh ser2net instance is +started for each acquire, so repeated acquires by the same user on +the same board append to the same file. + +LG_SERIAL_TRACE_HEXDUMP +~~~~~~~~~~~~~~~~~~~~~~~ +When set to ``1`` (and ``LG_SERIAL_TRACE_DIR`` is also set), the +exporter additionally enables ser2net's hexdump format for the +trace. This prepends a timestamp to every line at the cost of +producing hex+ASCII output instead of readable text. Use this +when correlating to the millisecond matters more than readability. + EXAMPLES -------- diff --git a/doc/usage.rst b/doc/usage.rst index 37527fb40..25848f95b 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -166,6 +166,145 @@ allocated before returning. A reservation will time out after a short time, if it is neither refreshed nor used by locked places. +Logging Serial Traffic on the Exporter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When several developers (or CI jobs) share the same boards through a single +coordinator, it is often useful for the lab admin to keep an independent +record of every serial-console session. The per-client ``--logfile`` option +only records output for the user who set it, so it does not help the operator +answer questions like *which commands left this board in a bad state?* after +the fact. + +To enable a centralised log on the exporter host, set the +``LG_SERIAL_TRACE_DIR`` environment variable before starting +``labgrid-exporter``: + +.. code-block:: bash + + $ export LG_SERIAL_TRACE_DIR=/var/log/labgrid/serial + $ labgrid-exporter my-config.yaml + +For each acquire, the exporter asks ``ser2net`` to record both sides of the +serial connection to a file named ``-.log`` under that directory, +where ```` is the resource group name (falling back to the basename of +the device path if the resource has no group) and ```` is the host/user +identity reported by the coordinator (slashes rewritten to underscores so the +result is filesystem safe, or ``unknown`` for older coordinators that do not +send a user). + +Because ``ser2net`` is started fresh on each acquire and stopped on release, +the trace covers exactly one session per acquire. Repeated acquires by the +same user on the same board append to the same file, so a long-running +developer ends up with a single, continuous log per board. + +This feature complements rather than replaces user-side logging (``--logfile`` +on the client, ``--lg-log`` on the pytest plugin, or ``ConsoleLoggingReporter`` +when using labgrid as a library): the user-side logs are what the developer +sees in their terminal and chooses what to capture, while the exporter trace +is the operator's independent record that the user cannot influence. + +A default-mode trace looks like this (user typed ``echo Me again`` at the +U-Boot prompt; each character of the input appears twice because ``tcp`` and +``term`` are written into the same stream, so the typed character and the +board's local echo are interleaved):: + + 2026/05/13 06:58:50 OPEN (ipv6,::ffff:192.168.4.7,60208) + ... + Press SPACE to abort autoboot in 2 seconds + => eecchhoo MMee aaggaaiinn + + Me again + => 2026/05/13 06:59:06 CLOSE netcon (network read close) + 2026/05/13 06:59:06 CLOSE port (All users disconnected) + +Connection events (``OPEN`` when ser2net accepts the client connection, +``CLOSE`` when the acquire is released) are timestamped by ser2net, giving +the session boundary that the lab admin can use to correlate a trace with +other audit logs. The console traffic itself is recorded verbatim with both +directions interleaved as they arrive, so the file reads like a transcript of +what was on the wire. + +If per-line timestamps are needed (for example, when correlating to the +millisecond against another log source), additionally set +``LG_SERIAL_TRACE_HEXDUMP=1`` before starting ``labgrid-exporter``. The +exporter then enables ser2net's hexdump format for the trace, which prepends +a timestamp and a direction tag to every line at the cost of producing +hex+ASCII output instead of readable text. The direction tag is ``term`` for +bytes the board emitted and ``tcp`` for bytes the client sent. A hex trace +of the same ``echo Hello Again`` session looks like this — each typed +character shows up as a ``tcp`` line immediately followed by a ``term`` line +as the board echoes it, then the board's output appears as a single +multi-byte ``term`` line:: + + 2026/05/13 07:21:02 OPEN (ipv6,::ffff:192.168.4.7,44566) + ... + 2026/05/13 07:21:11 tcp 65 |e| + 2026/05/13 07:21:11 term 65 |e| + 2026/05/13 07:21:11 tcp 63 |c| + 2026/05/13 07:21:11 term 63 |c| + 2026/05/13 07:21:11 tcp 68 |h| + 2026/05/13 07:21:11 term 68 |h| + 2026/05/13 07:21:11 tcp 6f |o| + 2026/05/13 07:21:11 term 6f |o| + 2026/05/13 07:21:11 tcp 20 | | + 2026/05/13 07:21:11 term 20 | | + ... + 2026/05/13 07:21:15 tcp 0a |.| + 2026/05/13 07:21:15 term 0d 0a 48 65 6c 6c 6f 20 |..Hello | + 2026/05/13 07:21:15 term 41 67 61 69 6e 0d 0a 3d |Again..=| + 2026/05/13 07:21:15 term 3e 20 |> | + 2026/05/13 07:21:16 CLOSE netcon (network read close) + +Setup with systemd +^^^^^^^^^^^^^^^^^^ + +When the exporter runs from a systemd unit, drop the variables into the +``[Service]`` section so they survive restarts: + +.. code-block:: ini + + [Service] + Environment=LG_SERIAL_TRACE_DIR=/var/log/labgrid/serial + # Optional, for per-line timestamps: + #Environment=LG_SERIAL_TRACE_HEXDUMP=1 + +Apply with ``systemctl daemon-reload && systemctl restart labgrid-exporter``. +Environment variables are read only when the exporter process starts, so a +config change needs a restart, not just a reload. + +Permissions +^^^^^^^^^^^ + +Trace files are created with the exporter user's umask (typically mode +``0600``, owned by whatever user the systemd unit runs as). An admin reading +them needs either ``sudo`` privileges, membership in the exporter user's +group, or the umask widened so a known operator group can read. Keep this in +mind when oncall reaches for the log: ``cat`` as your normal user is not +enough. + +Log rotation is intentionally not handled by the exporter itself: files live +on the lab host where the admin already manages the filesystem, so rotation +is best left to ``logrotate`` or an equivalent tool pointed at +``LG_SERIAL_TRACE_DIR``. Because ser2net opens the trace file in append mode +on each acquire, rotating between sessions (e.g. with ``logrotate``'s +``copytruncate`` or ``daily`` options) is safe. + +Privacy and disk usage +^^^^^^^^^^^^^^^^^^^^^^ + +The trace captures **everything** that flows on the serial console, which can +include passwords typed at a U-Boot or login prompt, ssh keys pasted into a +shell, and anything else the user types or that the board prints. Treat +``LG_SERIAL_TRACE_DIR`` as an audit log: restrict the directory's permissions, +document its existence to users, and check the contents against your +organisation's privacy policy before turning it on in production. + +Disk usage is modest in default mode (the trace is the same byte stream as +the console, plus the OPEN/CLOSE markers) but grows roughly ten-fold in +hexdump mode, where each byte becomes a full timestamped line. Plan rotation +accordingly when enabling ``LG_SERIAL_TRACE_HEXDUMP=1`` on a busy lab. + Library ------- labgrid can be used directly as a Python library, without the infrastructure diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index a63a21482..113f28daa 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -647,6 +647,8 @@ async def _acquire_resource(self, place, resource): request.group_name = resource.path[1] request.resource_name = resource.path[3] request.place_name = place.name + if place.acquired: + request.user = place.acquired cmd = ExporterCommand(request) self.get_exporter_by_name(resource.path[0]).queue.put_nowait(cmd) await cmd.wait() diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index 0e48f1942..ec189cfa2 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -67,6 +67,8 @@ class ResourceExport(ResourceEntry): host = attr.ib(default=gethostname(), validator=attr.validators.instance_of(str)) proxy = attr.ib(default=None) proxy_required = attr.ib(default=False) + group_name = attr.ib(default="") + user = attr.ib(default=None, init=False) local = attr.ib(init=False) local_params = attr.ib(init=False) start_params = attr.ib(init=False) @@ -230,6 +232,43 @@ def _get_params(self): }, } + @staticmethod + def _build_trace_args(group_name, user, path): + """Return ser2net YAML args for trace logging, or [] if disabled + + Reads LG_SERIAL_TRACE_DIR; when set, creates the directory and + builds a per-board, per-user file path under it, then returns + the YAML option pairs needed to enable trace-both for that + file. The board comes from the resource's group name when + available, falling back to the basename of the device path so + the filename is still meaningful. The user is the host/user + identity passed in by the coordinator (slashes are rewritten + to underscores so it is filesystem-safe), or ``unknown`` when + the coordinator did not send one. + + Setting LG_SERIAL_TRACE_HEXDUMP=1 additionally enables ser2net's + hexdump format for the trace, which prepends a timestamp to + every line at the cost of producing hex+ASCII rather than raw + text. Without it, only the OPEN and CLOSE session markers are + timestamped, but the traffic remains readable. + """ + trace_dir = os.environ.get("LG_SERIAL_TRACE_DIR") + if not trace_dir: + return [] + os.makedirs(trace_dir, exist_ok=True) + board = group_name or os.path.basename(path) + user_label = (user or "unknown").replace("/", "_") + trace_path = os.path.join(trace_dir, f"{board}-{user_label}.log") + args = [ + "-Y", + f" trace-both: {trace_path}", + "-Y", + " trace-both-timestamp: true", + ] + if os.environ.get("LG_SERIAL_TRACE_HEXDUMP") == "1": + args += ["-Y", " trace-both-hexdump: true"] + return args + def _start(self, start_params): """Start ``ser2net`` subprocess""" assert self.local.avail @@ -263,6 +302,12 @@ def _start(self, start_params): "-Y", " max-connections: 10", ] + # If LG_SERIAL_TRACE_DIR is set, ask ser2net to log all + # serial traffic for this device. Useful for centralised + # audit on the exporter host. ser2net is started fresh + # on each acquire and stopped on release, so the trace + # file scope is one acquire session. + cmd += self._build_trace_args(self.group_name, self.user, start_params["path"]) else: cmd = [ self.ser2net_bin, @@ -912,6 +957,7 @@ async def message_pump(self): out_message.set_acquired_request.group_name, out_message.set_acquired_request.resource_name, out_message.set_acquired_request.place_name, + out_message.set_acquired_request.user or None, ) else: await self.release( @@ -952,7 +998,7 @@ async def message_pump(self): # perhaps with queue join/task_done # this should be a command from the coordinator - async def acquire(self, group_name, resource_name, place_name): + async def acquire(self, group_name, resource_name, place_name, user=None): resource = self.groups.get(group_name, {}).get(resource_name) if resource is None: raise UnknownResourceError( @@ -964,6 +1010,11 @@ async def acquire(self, group_name, resource_name, place_name): f"Resource {group_name}/{resource_name} is already acquired by {resource.acquired}" ) + # Stash the acquiring user so ResourceExport subclasses can use it + # (e.g. for per-user trace files). Older coordinators don't send + # this field, in which case it stays None. + if isinstance(resource, ResourceExport): + resource.user = user try: resource.acquire(place_name) finally: @@ -1023,7 +1074,11 @@ async def add_resource(self, group_name, resource_name, cls, params): proxy_req = self.isolated if issubclass(export_cls, ResourceExport): res = group[resource_name] = export_cls( - config, host=self.hostname, proxy=getfqdn(), proxy_required=proxy_req + config, + host=self.hostname, + proxy=getfqdn(), + proxy_required=proxy_req, + group_name=group_name, ) res.poll() else: diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2.py b/labgrid/remote/generated/labgrid_coordinator_pb2.py index 37652bff7..f0ec652d6 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2.py +++ b/labgrid/remote/generated/labgrid_coordinator_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19labgrid-coordinator.proto\x12\x07labgrid\"\x8a\x01\n\x0f\x43lientInMessage\x12\x1d\n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12\'\n\tsubscribe\x18\x03 \x01(\x0b\x32\x12.labgrid.SubscribeH\x00\x42\x06\n\x04kind\"\x12\n\x04Sync\x12\n\n\x02id\x18\x01 \x01(\x04\",\n\x0bStartupDone\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\tSubscribe\x12\x1b\n\x0eis_unsubscribe\x18\x01 \x01(\x08H\x01\x88\x01\x01\x12\x14\n\nall_places\x18\x02 \x01(\x08H\x00\x12\x17\n\rall_resources\x18\x03 \x01(\x08H\x00\x42\x06\n\x04kindB\x11\n\x0f_is_unsubscribe\"g\n\x10\x43lientOutMessage\x12 \n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x88\x01\x01\x12(\n\x07updates\x18\x02 \x03(\x0b\x32\x17.labgrid.UpdateResponseB\x07\n\x05_sync\"\xa5\x01\n\x0eUpdateResponse\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12.\n\x0c\x64\x65l_resource\x18\x02 \x01(\x0b\x32\x16.labgrid.Resource.PathH\x00\x12\x1f\n\x05place\x18\x03 \x01(\x0b\x32\x0e.labgrid.PlaceH\x00\x12\x13\n\tdel_place\x18\x04 \x01(\tH\x00\x42\x06\n\x04kind\"\x9a\x01\n\x11\x45xporterInMessage\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12-\n\x08response\x18\x03 \x01(\x0b\x32\x19.labgrid.ExporterResponseH\x00\x42\x06\n\x04kind\"\x9e\x03\n\x08Resource\x12$\n\x04path\x18\x01 \x01(\x0b\x32\x16.labgrid.Resource.Path\x12\x0b\n\x03\x63ls\x18\x02 \x01(\t\x12-\n\x06params\x18\x03 \x03(\x0b\x32\x1d.labgrid.Resource.ParamsEntry\x12+\n\x05\x65xtra\x18\x04 \x03(\x0b\x32\x1c.labgrid.Resource.ExtraEntry\x12\x10\n\x08\x61\x63quired\x18\x05 \x01(\t\x12\r\n\x05\x61vail\x18\x06 \x01(\x08\x1a_\n\x04Path\x12\x1a\n\rexporter_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ngroup_name\x18\x02 \x01(\t\x12\x15\n\rresource_name\x18\x03 \x01(\tB\x10\n\x0e_exporter_name\x1a@\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\x1a?\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\"\x82\x01\n\x08MapValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nuint_value\x18\x03 \x01(\x04H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x05 \x01(\tH\x00\x42\x06\n\x04kind\"C\n\x10\x45xporterResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x06reason\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_reason\"\x18\n\x05Hello\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x82\x01\n\x12\x45xporterOutMessage\x12\x1f\n\x05hello\x18\x01 \x01(\x0b\x32\x0e.labgrid.HelloH\x00\x12\x43\n\x14set_acquired_request\x18\x02 \x01(\x0b\x32#.labgrid.ExporterSetAcquiredRequestH\x00\x42\x06\n\x04kind\"o\n\x1a\x45xporterSetAcquiredRequest\x12\x12\n\ngroup_name\x18\x01 \x01(\t\x12\x15\n\rresource_name\x18\x02 \x01(\t\x12\x17\n\nplace_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_place_name\"\x1f\n\x0f\x41\x64\x64PlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x12\n\x10\x41\x64\x64PlaceResponse\"\"\n\x12\x44\x65letePlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x15\n\x13\x44\x65letePlaceResponse\"\x12\n\x10GetPlacesRequest\"3\n\x11GetPlacesResponse\x12\x1e\n\x06places\x18\x01 \x03(\x0b\x32\x0e.labgrid.Place\"\xd2\x02\n\x05Place\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61liases\x18\x02 \x03(\t\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12&\n\x04tags\x18\x04 \x03(\x0b\x32\x18.labgrid.Place.TagsEntry\x12\'\n\x07matches\x18\x05 \x03(\x0b\x32\x16.labgrid.ResourceMatch\x12\x15\n\x08\x61\x63quired\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x1a\n\x12\x61\x63quired_resources\x18\x07 \x03(\t\x12\x0f\n\x07\x61llowed\x18\x08 \x03(\t\x12\x0f\n\x07\x63reated\x18\t \x01(\x01\x12\x0f\n\x07\x63hanged\x18\n \x01(\x01\x12\x18\n\x0breservation\x18\x0b \x01(\tH\x01\x88\x01\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0b\n\t_acquiredB\x0e\n\x0c_reservation\"y\n\rResourceMatch\x12\x10\n\x08\x65xporter\x18\x01 \x01(\t\x12\r\n\x05group\x18\x02 \x01(\t\x12\x0b\n\x03\x63ls\x18\x03 \x01(\t\x12\x11\n\x04name\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06rename\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_rename\"8\n\x14\x41\x64\x64PlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x17\n\x15\x41\x64\x64PlaceAliasResponse\";\n\x17\x44\x65letePlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x1a\n\x18\x44\x65letePlaceAliasResponse\"\x8b\x01\n\x13SetPlaceTagsRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x34\n\x04tags\x18\x02 \x03(\x0b\x32&.labgrid.SetPlaceTagsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x16\n\x14SetPlaceTagsResponse\"<\n\x16SetPlaceCommentRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\"\x19\n\x17SetPlaceCommentResponse\"Z\n\x14\x41\x64\x64PlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x17\n\x15\x41\x64\x64PlaceMatchResponse\"]\n\x17\x44\x65letePlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x1a\n\x18\x44\x65letePlaceMatchResponse\"(\n\x13\x41\x63quirePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\"\x16\n\x14\x41\x63quirePlaceResponse\"L\n\x13ReleasePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x15\n\x08\x66romuser\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_fromuser\"\x16\n\x14ReleasePlaceResponse\"4\n\x11\x41llowPlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0c\n\x04user\x18\x02 \x01(\t\"\x14\n\x12\x41llowPlaceResponse\"\xb6\x01\n\x18\x43reateReservationRequest\x12?\n\x07\x66ilters\x18\x01 \x03(\x0b\x32..labgrid.CreateReservationRequest.FiltersEntry\x12\x0c\n\x04prio\x18\x02 \x01(\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\"F\n\x19\x43reateReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"\xcd\x03\n\x0bReservation\x12\r\n\x05owner\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\x05\x12\x0c\n\x04prio\x18\x04 \x01(\x01\x12\x32\n\x07\x66ilters\x18\x05 \x03(\x0b\x32!.labgrid.Reservation.FiltersEntry\x12:\n\x0b\x61llocations\x18\x06 \x03(\x0b\x32%.labgrid.Reservation.AllocationsEntry\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x01\x12\x0f\n\x07timeout\x18\x08 \x01(\x01\x1ap\n\x06\x46ilter\x12\x37\n\x06\x66ilter\x18\x01 \x03(\x0b\x32\'.labgrid.Reservation.Filter.FilterEntry\x1a-\n\x0b\x46ilterEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\x1a\x32\n\x10\x41llocationsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\")\n\x18\x43\x61ncelReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"\x1b\n\x19\x43\x61ncelReservationResponse\"\'\n\x16PollReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"D\n\x17PollReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"E\n\x17GetReservationsResponse\x12*\n\x0creservations\x18\x01 \x03(\x0b\x32\x14.labgrid.Reservation\"\x18\n\x16GetReservationsRequest2\xd2\x0b\n\x0b\x43oordinator\x12I\n\x0c\x43lientStream\x12\x18.labgrid.ClientInMessage\x1a\x19.labgrid.ClientOutMessage\"\x00(\x01\x30\x01\x12O\n\x0e\x45xporterStream\x12\x1a.labgrid.ExporterInMessage\x1a\x1b.labgrid.ExporterOutMessage\"\x00(\x01\x30\x01\x12\x41\n\x08\x41\x64\x64Place\x12\x18.labgrid.AddPlaceRequest\x1a\x19.labgrid.AddPlaceResponse\"\x00\x12J\n\x0b\x44\x65letePlace\x12\x1b.labgrid.DeletePlaceRequest\x1a\x1c.labgrid.DeletePlaceResponse\"\x00\x12\x44\n\tGetPlaces\x12\x19.labgrid.GetPlacesRequest\x1a\x1a.labgrid.GetPlacesResponse\"\x00\x12P\n\rAddPlaceAlias\x12\x1d.labgrid.AddPlaceAliasRequest\x1a\x1e.labgrid.AddPlaceAliasResponse\"\x00\x12Y\n\x10\x44\x65letePlaceAlias\x12 .labgrid.DeletePlaceAliasRequest\x1a!.labgrid.DeletePlaceAliasResponse\"\x00\x12M\n\x0cSetPlaceTags\x12\x1c.labgrid.SetPlaceTagsRequest\x1a\x1d.labgrid.SetPlaceTagsResponse\"\x00\x12V\n\x0fSetPlaceComment\x12\x1f.labgrid.SetPlaceCommentRequest\x1a .labgrid.SetPlaceCommentResponse\"\x00\x12P\n\rAddPlaceMatch\x12\x1d.labgrid.AddPlaceMatchRequest\x1a\x1e.labgrid.AddPlaceMatchResponse\"\x00\x12Y\n\x10\x44\x65letePlaceMatch\x12 .labgrid.DeletePlaceMatchRequest\x1a!.labgrid.DeletePlaceMatchResponse\"\x00\x12M\n\x0c\x41\x63quirePlace\x12\x1c.labgrid.AcquirePlaceRequest\x1a\x1d.labgrid.AcquirePlaceResponse\"\x00\x12M\n\x0cReleasePlace\x12\x1c.labgrid.ReleasePlaceRequest\x1a\x1d.labgrid.ReleasePlaceResponse\"\x00\x12G\n\nAllowPlace\x12\x1a.labgrid.AllowPlaceRequest\x1a\x1b.labgrid.AllowPlaceResponse\"\x00\x12\\\n\x11\x43reateReservation\x12!.labgrid.CreateReservationRequest\x1a\".labgrid.CreateReservationResponse\"\x00\x12\\\n\x11\x43\x61ncelReservation\x12!.labgrid.CancelReservationRequest\x1a\".labgrid.CancelReservationResponse\"\x00\x12V\n\x0fPollReservation\x12\x1f.labgrid.PollReservationRequest\x1a .labgrid.PollReservationResponse\"\x00\x12V\n\x0fGetReservations\x12\x1f.labgrid.GetReservationsRequest\x1a .labgrid.GetReservationsResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19labgrid-coordinator.proto\x12\x07labgrid\"\x8a\x01\n\x0f\x43lientInMessage\x12\x1d\n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12\'\n\tsubscribe\x18\x03 \x01(\x0b\x32\x12.labgrid.SubscribeH\x00\x42\x06\n\x04kind\"\x12\n\x04Sync\x12\n\n\x02id\x18\x01 \x01(\x04\",\n\x0bStartupDone\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\tSubscribe\x12\x1b\n\x0eis_unsubscribe\x18\x01 \x01(\x08H\x01\x88\x01\x01\x12\x14\n\nall_places\x18\x02 \x01(\x08H\x00\x12\x17\n\rall_resources\x18\x03 \x01(\x08H\x00\x42\x06\n\x04kindB\x11\n\x0f_is_unsubscribe\"g\n\x10\x43lientOutMessage\x12 \n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x88\x01\x01\x12(\n\x07updates\x18\x02 \x03(\x0b\x32\x17.labgrid.UpdateResponseB\x07\n\x05_sync\"\xa5\x01\n\x0eUpdateResponse\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12.\n\x0c\x64\x65l_resource\x18\x02 \x01(\x0b\x32\x16.labgrid.Resource.PathH\x00\x12\x1f\n\x05place\x18\x03 \x01(\x0b\x32\x0e.labgrid.PlaceH\x00\x12\x13\n\tdel_place\x18\x04 \x01(\tH\x00\x42\x06\n\x04kind\"\x9a\x01\n\x11\x45xporterInMessage\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12-\n\x08response\x18\x03 \x01(\x0b\x32\x19.labgrid.ExporterResponseH\x00\x42\x06\n\x04kind\"\x9e\x03\n\x08Resource\x12$\n\x04path\x18\x01 \x01(\x0b\x32\x16.labgrid.Resource.Path\x12\x0b\n\x03\x63ls\x18\x02 \x01(\t\x12-\n\x06params\x18\x03 \x03(\x0b\x32\x1d.labgrid.Resource.ParamsEntry\x12+\n\x05\x65xtra\x18\x04 \x03(\x0b\x32\x1c.labgrid.Resource.ExtraEntry\x12\x10\n\x08\x61\x63quired\x18\x05 \x01(\t\x12\r\n\x05\x61vail\x18\x06 \x01(\x08\x1a_\n\x04Path\x12\x1a\n\rexporter_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ngroup_name\x18\x02 \x01(\t\x12\x15\n\rresource_name\x18\x03 \x01(\tB\x10\n\x0e_exporter_name\x1a@\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\x1a?\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\"\x82\x01\n\x08MapValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nuint_value\x18\x03 \x01(\x04H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x05 \x01(\tH\x00\x42\x06\n\x04kind\"C\n\x10\x45xporterResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x06reason\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_reason\"\x18\n\x05Hello\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x82\x01\n\x12\x45xporterOutMessage\x12\x1f\n\x05hello\x18\x01 \x01(\x0b\x32\x0e.labgrid.HelloH\x00\x12\x43\n\x14set_acquired_request\x18\x02 \x01(\x0b\x32#.labgrid.ExporterSetAcquiredRequestH\x00\x42\x06\n\x04kind\"}\n\x1a\x45xporterSetAcquiredRequest\x12\x12\n\ngroup_name\x18\x01 \x01(\t\x12\x15\n\rresource_name\x18\x02 \x01(\t\x12\x17\n\nplace_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x0c\n\x04user\x18\x04 \x01(\tB\r\n\x0b_place_name\"\x1f\n\x0f\x41\x64\x64PlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x12\n\x10\x41\x64\x64PlaceResponse\"\"\n\x12\x44\x65letePlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x15\n\x13\x44\x65letePlaceResponse\"\x12\n\x10GetPlacesRequest\"3\n\x11GetPlacesResponse\x12\x1e\n\x06places\x18\x01 \x03(\x0b\x32\x0e.labgrid.Place\"\xd2\x02\n\x05Place\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61liases\x18\x02 \x03(\t\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12&\n\x04tags\x18\x04 \x03(\x0b\x32\x18.labgrid.Place.TagsEntry\x12\'\n\x07matches\x18\x05 \x03(\x0b\x32\x16.labgrid.ResourceMatch\x12\x15\n\x08\x61\x63quired\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x1a\n\x12\x61\x63quired_resources\x18\x07 \x03(\t\x12\x0f\n\x07\x61llowed\x18\x08 \x03(\t\x12\x0f\n\x07\x63reated\x18\t \x01(\x01\x12\x0f\n\x07\x63hanged\x18\n \x01(\x01\x12\x18\n\x0breservation\x18\x0b \x01(\tH\x01\x88\x01\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0b\n\t_acquiredB\x0e\n\x0c_reservation\"y\n\rResourceMatch\x12\x10\n\x08\x65xporter\x18\x01 \x01(\t\x12\r\n\x05group\x18\x02 \x01(\t\x12\x0b\n\x03\x63ls\x18\x03 \x01(\t\x12\x11\n\x04name\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06rename\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_rename\"8\n\x14\x41\x64\x64PlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x17\n\x15\x41\x64\x64PlaceAliasResponse\";\n\x17\x44\x65letePlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x1a\n\x18\x44\x65letePlaceAliasResponse\"\x8b\x01\n\x13SetPlaceTagsRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x34\n\x04tags\x18\x02 \x03(\x0b\x32&.labgrid.SetPlaceTagsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x16\n\x14SetPlaceTagsResponse\"<\n\x16SetPlaceCommentRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\"\x19\n\x17SetPlaceCommentResponse\"Z\n\x14\x41\x64\x64PlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x17\n\x15\x41\x64\x64PlaceMatchResponse\"]\n\x17\x44\x65letePlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x1a\n\x18\x44\x65letePlaceMatchResponse\"(\n\x13\x41\x63quirePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\"\x16\n\x14\x41\x63quirePlaceResponse\"L\n\x13ReleasePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x15\n\x08\x66romuser\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_fromuser\"\x16\n\x14ReleasePlaceResponse\"4\n\x11\x41llowPlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0c\n\x04user\x18\x02 \x01(\t\"\x14\n\x12\x41llowPlaceResponse\"\xb6\x01\n\x18\x43reateReservationRequest\x12?\n\x07\x66ilters\x18\x01 \x03(\x0b\x32..labgrid.CreateReservationRequest.FiltersEntry\x12\x0c\n\x04prio\x18\x02 \x01(\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\"F\n\x19\x43reateReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"\xcd\x03\n\x0bReservation\x12\r\n\x05owner\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\x05\x12\x0c\n\x04prio\x18\x04 \x01(\x01\x12\x32\n\x07\x66ilters\x18\x05 \x03(\x0b\x32!.labgrid.Reservation.FiltersEntry\x12:\n\x0b\x61llocations\x18\x06 \x03(\x0b\x32%.labgrid.Reservation.AllocationsEntry\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x01\x12\x0f\n\x07timeout\x18\x08 \x01(\x01\x1ap\n\x06\x46ilter\x12\x37\n\x06\x66ilter\x18\x01 \x03(\x0b\x32\'.labgrid.Reservation.Filter.FilterEntry\x1a-\n\x0b\x46ilterEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\x1a\x32\n\x10\x41llocationsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\")\n\x18\x43\x61ncelReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"\x1b\n\x19\x43\x61ncelReservationResponse\"\'\n\x16PollReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"D\n\x17PollReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"E\n\x17GetReservationsResponse\x12*\n\x0creservations\x18\x01 \x03(\x0b\x32\x14.labgrid.Reservation\"\x18\n\x16GetReservationsRequest2\xd2\x0b\n\x0b\x43oordinator\x12I\n\x0c\x43lientStream\x12\x18.labgrid.ClientInMessage\x1a\x19.labgrid.ClientOutMessage\"\x00(\x01\x30\x01\x12O\n\x0e\x45xporterStream\x12\x1a.labgrid.ExporterInMessage\x1a\x1b.labgrid.ExporterOutMessage\"\x00(\x01\x30\x01\x12\x41\n\x08\x41\x64\x64Place\x12\x18.labgrid.AddPlaceRequest\x1a\x19.labgrid.AddPlaceResponse\"\x00\x12J\n\x0b\x44\x65letePlace\x12\x1b.labgrid.DeletePlaceRequest\x1a\x1c.labgrid.DeletePlaceResponse\"\x00\x12\x44\n\tGetPlaces\x12\x19.labgrid.GetPlacesRequest\x1a\x1a.labgrid.GetPlacesResponse\"\x00\x12P\n\rAddPlaceAlias\x12\x1d.labgrid.AddPlaceAliasRequest\x1a\x1e.labgrid.AddPlaceAliasResponse\"\x00\x12Y\n\x10\x44\x65letePlaceAlias\x12 .labgrid.DeletePlaceAliasRequest\x1a!.labgrid.DeletePlaceAliasResponse\"\x00\x12M\n\x0cSetPlaceTags\x12\x1c.labgrid.SetPlaceTagsRequest\x1a\x1d.labgrid.SetPlaceTagsResponse\"\x00\x12V\n\x0fSetPlaceComment\x12\x1f.labgrid.SetPlaceCommentRequest\x1a .labgrid.SetPlaceCommentResponse\"\x00\x12P\n\rAddPlaceMatch\x12\x1d.labgrid.AddPlaceMatchRequest\x1a\x1e.labgrid.AddPlaceMatchResponse\"\x00\x12Y\n\x10\x44\x65letePlaceMatch\x12 .labgrid.DeletePlaceMatchRequest\x1a!.labgrid.DeletePlaceMatchResponse\"\x00\x12M\n\x0c\x41\x63quirePlace\x12\x1c.labgrid.AcquirePlaceRequest\x1a\x1d.labgrid.AcquirePlaceResponse\"\x00\x12M\n\x0cReleasePlace\x12\x1c.labgrid.ReleasePlaceRequest\x1a\x1d.labgrid.ReleasePlaceResponse\"\x00\x12G\n\nAllowPlace\x12\x1a.labgrid.AllowPlaceRequest\x1a\x1b.labgrid.AllowPlaceResponse\"\x00\x12\\\n\x11\x43reateReservation\x12!.labgrid.CreateReservationRequest\x1a\".labgrid.CreateReservationResponse\"\x00\x12\\\n\x11\x43\x61ncelReservation\x12!.labgrid.CancelReservationRequest\x1a\".labgrid.CancelReservationResponse\"\x00\x12V\n\x0fPollReservation\x12\x1f.labgrid.PollReservationRequest\x1a .labgrid.PollReservationResponse\"\x00\x12V\n\x0fGetReservations\x12\x1f.labgrid.GetReservationsRequest\x1a .labgrid.GetReservationsResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -68,91 +68,91 @@ _globals['_EXPORTEROUTMESSAGE']._serialized_start=1437 _globals['_EXPORTEROUTMESSAGE']._serialized_end=1567 _globals['_EXPORTERSETACQUIREDREQUEST']._serialized_start=1569 - _globals['_EXPORTERSETACQUIREDREQUEST']._serialized_end=1680 - _globals['_ADDPLACEREQUEST']._serialized_start=1682 - _globals['_ADDPLACEREQUEST']._serialized_end=1713 - _globals['_ADDPLACERESPONSE']._serialized_start=1715 - _globals['_ADDPLACERESPONSE']._serialized_end=1733 - _globals['_DELETEPLACEREQUEST']._serialized_start=1735 - _globals['_DELETEPLACEREQUEST']._serialized_end=1769 - _globals['_DELETEPLACERESPONSE']._serialized_start=1771 - _globals['_DELETEPLACERESPONSE']._serialized_end=1792 - _globals['_GETPLACESREQUEST']._serialized_start=1794 - _globals['_GETPLACESREQUEST']._serialized_end=1812 - _globals['_GETPLACESRESPONSE']._serialized_start=1814 - _globals['_GETPLACESRESPONSE']._serialized_end=1865 - _globals['_PLACE']._serialized_start=1868 - _globals['_PLACE']._serialized_end=2206 - _globals['_PLACE_TAGSENTRY']._serialized_start=2134 - _globals['_PLACE_TAGSENTRY']._serialized_end=2177 - _globals['_RESOURCEMATCH']._serialized_start=2208 - _globals['_RESOURCEMATCH']._serialized_end=2329 - _globals['_ADDPLACEALIASREQUEST']._serialized_start=2331 - _globals['_ADDPLACEALIASREQUEST']._serialized_end=2387 - _globals['_ADDPLACEALIASRESPONSE']._serialized_start=2389 - _globals['_ADDPLACEALIASRESPONSE']._serialized_end=2412 - _globals['_DELETEPLACEALIASREQUEST']._serialized_start=2414 - _globals['_DELETEPLACEALIASREQUEST']._serialized_end=2473 - _globals['_DELETEPLACEALIASRESPONSE']._serialized_start=2475 - _globals['_DELETEPLACEALIASRESPONSE']._serialized_end=2501 - _globals['_SETPLACETAGSREQUEST']._serialized_start=2504 - _globals['_SETPLACETAGSREQUEST']._serialized_end=2643 - _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_start=2134 - _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_end=2177 - _globals['_SETPLACETAGSRESPONSE']._serialized_start=2645 - _globals['_SETPLACETAGSRESPONSE']._serialized_end=2667 - _globals['_SETPLACECOMMENTREQUEST']._serialized_start=2669 - _globals['_SETPLACECOMMENTREQUEST']._serialized_end=2729 - _globals['_SETPLACECOMMENTRESPONSE']._serialized_start=2731 - _globals['_SETPLACECOMMENTRESPONSE']._serialized_end=2756 - _globals['_ADDPLACEMATCHREQUEST']._serialized_start=2758 - _globals['_ADDPLACEMATCHREQUEST']._serialized_end=2848 - _globals['_ADDPLACEMATCHRESPONSE']._serialized_start=2850 - _globals['_ADDPLACEMATCHRESPONSE']._serialized_end=2873 - _globals['_DELETEPLACEMATCHREQUEST']._serialized_start=2875 - _globals['_DELETEPLACEMATCHREQUEST']._serialized_end=2968 - _globals['_DELETEPLACEMATCHRESPONSE']._serialized_start=2970 - _globals['_DELETEPLACEMATCHRESPONSE']._serialized_end=2996 - _globals['_ACQUIREPLACEREQUEST']._serialized_start=2998 - _globals['_ACQUIREPLACEREQUEST']._serialized_end=3038 - _globals['_ACQUIREPLACERESPONSE']._serialized_start=3040 - _globals['_ACQUIREPLACERESPONSE']._serialized_end=3062 - _globals['_RELEASEPLACEREQUEST']._serialized_start=3064 - _globals['_RELEASEPLACEREQUEST']._serialized_end=3140 - _globals['_RELEASEPLACERESPONSE']._serialized_start=3142 - _globals['_RELEASEPLACERESPONSE']._serialized_end=3164 - _globals['_ALLOWPLACEREQUEST']._serialized_start=3166 - _globals['_ALLOWPLACEREQUEST']._serialized_end=3218 - _globals['_ALLOWPLACERESPONSE']._serialized_start=3220 - _globals['_ALLOWPLACERESPONSE']._serialized_end=3240 - _globals['_CREATERESERVATIONREQUEST']._serialized_start=3243 - _globals['_CREATERESERVATIONREQUEST']._serialized_end=3425 - _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_start=3350 - _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_end=3425 - _globals['_CREATERESERVATIONRESPONSE']._serialized_start=3427 - _globals['_CREATERESERVATIONRESPONSE']._serialized_end=3497 - _globals['_RESERVATION']._serialized_start=3500 - _globals['_RESERVATION']._serialized_end=3961 - _globals['_RESERVATION_FILTER']._serialized_start=3720 - _globals['_RESERVATION_FILTER']._serialized_end=3832 - _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_start=3787 - _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_end=3832 - _globals['_RESERVATION_FILTERSENTRY']._serialized_start=3350 - _globals['_RESERVATION_FILTERSENTRY']._serialized_end=3425 - _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_start=3911 - _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_end=3961 - _globals['_CANCELRESERVATIONREQUEST']._serialized_start=3963 - _globals['_CANCELRESERVATIONREQUEST']._serialized_end=4004 - _globals['_CANCELRESERVATIONRESPONSE']._serialized_start=4006 - _globals['_CANCELRESERVATIONRESPONSE']._serialized_end=4033 - _globals['_POLLRESERVATIONREQUEST']._serialized_start=4035 - _globals['_POLLRESERVATIONREQUEST']._serialized_end=4074 - _globals['_POLLRESERVATIONRESPONSE']._serialized_start=4076 - _globals['_POLLRESERVATIONRESPONSE']._serialized_end=4144 - _globals['_GETRESERVATIONSRESPONSE']._serialized_start=4146 - _globals['_GETRESERVATIONSRESPONSE']._serialized_end=4215 - _globals['_GETRESERVATIONSREQUEST']._serialized_start=4217 - _globals['_GETRESERVATIONSREQUEST']._serialized_end=4241 - _globals['_COORDINATOR']._serialized_start=4244 - _globals['_COORDINATOR']._serialized_end=5734 + _globals['_EXPORTERSETACQUIREDREQUEST']._serialized_end=1694 + _globals['_ADDPLACEREQUEST']._serialized_start=1696 + _globals['_ADDPLACEREQUEST']._serialized_end=1727 + _globals['_ADDPLACERESPONSE']._serialized_start=1729 + _globals['_ADDPLACERESPONSE']._serialized_end=1747 + _globals['_DELETEPLACEREQUEST']._serialized_start=1749 + _globals['_DELETEPLACEREQUEST']._serialized_end=1783 + _globals['_DELETEPLACERESPONSE']._serialized_start=1785 + _globals['_DELETEPLACERESPONSE']._serialized_end=1806 + _globals['_GETPLACESREQUEST']._serialized_start=1808 + _globals['_GETPLACESREQUEST']._serialized_end=1826 + _globals['_GETPLACESRESPONSE']._serialized_start=1828 + _globals['_GETPLACESRESPONSE']._serialized_end=1879 + _globals['_PLACE']._serialized_start=1882 + _globals['_PLACE']._serialized_end=2220 + _globals['_PLACE_TAGSENTRY']._serialized_start=2148 + _globals['_PLACE_TAGSENTRY']._serialized_end=2191 + _globals['_RESOURCEMATCH']._serialized_start=2222 + _globals['_RESOURCEMATCH']._serialized_end=2343 + _globals['_ADDPLACEALIASREQUEST']._serialized_start=2345 + _globals['_ADDPLACEALIASREQUEST']._serialized_end=2401 + _globals['_ADDPLACEALIASRESPONSE']._serialized_start=2403 + _globals['_ADDPLACEALIASRESPONSE']._serialized_end=2426 + _globals['_DELETEPLACEALIASREQUEST']._serialized_start=2428 + _globals['_DELETEPLACEALIASREQUEST']._serialized_end=2487 + _globals['_DELETEPLACEALIASRESPONSE']._serialized_start=2489 + _globals['_DELETEPLACEALIASRESPONSE']._serialized_end=2515 + _globals['_SETPLACETAGSREQUEST']._serialized_start=2518 + _globals['_SETPLACETAGSREQUEST']._serialized_end=2657 + _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_start=2148 + _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_end=2191 + _globals['_SETPLACETAGSRESPONSE']._serialized_start=2659 + _globals['_SETPLACETAGSRESPONSE']._serialized_end=2681 + _globals['_SETPLACECOMMENTREQUEST']._serialized_start=2683 + _globals['_SETPLACECOMMENTREQUEST']._serialized_end=2743 + _globals['_SETPLACECOMMENTRESPONSE']._serialized_start=2745 + _globals['_SETPLACECOMMENTRESPONSE']._serialized_end=2770 + _globals['_ADDPLACEMATCHREQUEST']._serialized_start=2772 + _globals['_ADDPLACEMATCHREQUEST']._serialized_end=2862 + _globals['_ADDPLACEMATCHRESPONSE']._serialized_start=2864 + _globals['_ADDPLACEMATCHRESPONSE']._serialized_end=2887 + _globals['_DELETEPLACEMATCHREQUEST']._serialized_start=2889 + _globals['_DELETEPLACEMATCHREQUEST']._serialized_end=2982 + _globals['_DELETEPLACEMATCHRESPONSE']._serialized_start=2984 + _globals['_DELETEPLACEMATCHRESPONSE']._serialized_end=3010 + _globals['_ACQUIREPLACEREQUEST']._serialized_start=3012 + _globals['_ACQUIREPLACEREQUEST']._serialized_end=3052 + _globals['_ACQUIREPLACERESPONSE']._serialized_start=3054 + _globals['_ACQUIREPLACERESPONSE']._serialized_end=3076 + _globals['_RELEASEPLACEREQUEST']._serialized_start=3078 + _globals['_RELEASEPLACEREQUEST']._serialized_end=3154 + _globals['_RELEASEPLACERESPONSE']._serialized_start=3156 + _globals['_RELEASEPLACERESPONSE']._serialized_end=3178 + _globals['_ALLOWPLACEREQUEST']._serialized_start=3180 + _globals['_ALLOWPLACEREQUEST']._serialized_end=3232 + _globals['_ALLOWPLACERESPONSE']._serialized_start=3234 + _globals['_ALLOWPLACERESPONSE']._serialized_end=3254 + _globals['_CREATERESERVATIONREQUEST']._serialized_start=3257 + _globals['_CREATERESERVATIONREQUEST']._serialized_end=3439 + _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_start=3364 + _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_end=3439 + _globals['_CREATERESERVATIONRESPONSE']._serialized_start=3441 + _globals['_CREATERESERVATIONRESPONSE']._serialized_end=3511 + _globals['_RESERVATION']._serialized_start=3514 + _globals['_RESERVATION']._serialized_end=3975 + _globals['_RESERVATION_FILTER']._serialized_start=3734 + _globals['_RESERVATION_FILTER']._serialized_end=3846 + _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_start=3801 + _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_end=3846 + _globals['_RESERVATION_FILTERSENTRY']._serialized_start=3364 + _globals['_RESERVATION_FILTERSENTRY']._serialized_end=3439 + _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_start=3925 + _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_end=3975 + _globals['_CANCELRESERVATIONREQUEST']._serialized_start=3977 + _globals['_CANCELRESERVATIONREQUEST']._serialized_end=4018 + _globals['_CANCELRESERVATIONRESPONSE']._serialized_start=4020 + _globals['_CANCELRESERVATIONRESPONSE']._serialized_end=4047 + _globals['_POLLRESERVATIONREQUEST']._serialized_start=4049 + _globals['_POLLRESERVATIONREQUEST']._serialized_end=4088 + _globals['_POLLRESERVATIONRESPONSE']._serialized_start=4090 + _globals['_POLLRESERVATIONRESPONSE']._serialized_end=4158 + _globals['_GETRESERVATIONSRESPONSE']._serialized_start=4160 + _globals['_GETRESERVATIONSRESPONSE']._serialized_end=4229 + _globals['_GETRESERVATIONSREQUEST']._serialized_start=4231 + _globals['_GETRESERVATIONSREQUEST']._serialized_end=4255 + _globals['_COORDINATOR']._serialized_start=4258 + _globals['_COORDINATOR']._serialized_end=5748 # @@protoc_insertion_point(module_scope) diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2.pyi b/labgrid/remote/generated/labgrid_coordinator_pb2.pyi index 366f4e438..aa4113d94 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2.pyi +++ b/labgrid/remote/generated/labgrid_coordinator_pb2.pyi @@ -145,14 +145,16 @@ class ExporterOutMessage(_message.Message): def __init__(self, hello: _Optional[_Union[Hello, _Mapping]] = ..., set_acquired_request: _Optional[_Union[ExporterSetAcquiredRequest, _Mapping]] = ...) -> None: ... class ExporterSetAcquiredRequest(_message.Message): - __slots__ = ("group_name", "resource_name", "place_name") + __slots__ = ("group_name", "resource_name", "place_name", "user") GROUP_NAME_FIELD_NUMBER: _ClassVar[int] RESOURCE_NAME_FIELD_NUMBER: _ClassVar[int] PLACE_NAME_FIELD_NUMBER: _ClassVar[int] + USER_FIELD_NUMBER: _ClassVar[int] group_name: str resource_name: str place_name: str - def __init__(self, group_name: _Optional[str] = ..., resource_name: _Optional[str] = ..., place_name: _Optional[str] = ...) -> None: ... + user: str + def __init__(self, group_name: _Optional[str] = ..., resource_name: _Optional[str] = ..., place_name: _Optional[str] = ..., user: _Optional[str] = ...) -> None: ... class AddPlaceRequest(_message.Message): __slots__ = ("name",) diff --git a/labgrid/remote/proto/labgrid-coordinator.proto b/labgrid/remote/proto/labgrid-coordinator.proto index e0585f7e1..77321575b 100644 --- a/labgrid/remote/proto/labgrid-coordinator.proto +++ b/labgrid/remote/proto/labgrid-coordinator.proto @@ -132,6 +132,12 @@ message ExporterSetAcquiredRequest { string group_name = 1; string resource_name = 2; optional string place_name = 3; + // The user that acquired the place, in "host/user" form. Set + // when place_name is set (acquire); empty on release. Allows the + // exporter to attribute resource use to a specific user, e.g. for + // per-user serial trace files. Plain (non-optional) so the field + // is supported by older protoc. + string user = 4; }; message AddPlaceRequest { diff --git a/man/labgrid-exporter.1 b/man/labgrid-exporter.1 index 92619f4d1..214571325 100644 --- a/man/labgrid-exporter.1 +++ b/man/labgrid-exporter.1 @@ -100,11 +100,34 @@ See <\X'tty: link https://labgrid.readthedocs.io/en/latest/configuration.html#ex for more information. .SS ENVIRONMENT VARIABLES .sp -The following environment variable can be used to configure labgrid\-exporter. +The following environment variables can be used to configure +labgrid\-exporter. .SS LG_COORDINATOR .sp This variable can be used to set the default coordinator in the format \fBHOST[:PORT]\fP (instead of using the \fB\-x\fP option). +.SS LG_SERIAL_TRACE_DIR +.sp +When set, the exporter records all serial\-port traffic for each +acquired resource into \fB/\-.log\fP, +where \fB\fP is the resource group name and \fB\fP is the +acquiring user identity reported by the coordinator (in +\fBhost/user\fP form, with \fB/\fP rewritten to \fB_\fP). Both directions +are captured verbatim, and ser2net writes timestamped OPEN/CLOSE +markers at the start and end of each session so the lab admin has +an independent record that is not affected by per\-client logging +options. +.sp +The directory is created on demand. A fresh ser2net instance is +started for each acquire, so repeated acquires by the same user on +the same board append to the same file. +.SS LG_SERIAL_TRACE_HEXDUMP +.sp +When set to \fB1\fP (and \fBLG_SERIAL_TRACE_DIR\fP is also set), the +exporter additionally enables ser2net\(aqs hexdump format for the +trace. This prepends a timestamp to every line at the cost of +producing hex+ASCII output instead of readable text. Use this +when correlating to the millisecond matters more than readability. .SS EXAMPLES .sp Start the exporter with the configuration file \fBmy\-config.yaml\fP: diff --git a/tests/test_exporter_trace.py b/tests/test_exporter_trace.py new file mode 100644 index 000000000..fdb0ed08e --- /dev/null +++ b/tests/test_exporter_trace.py @@ -0,0 +1,85 @@ +"""Tests for the LG_SERIAL_TRACE_DIR feature in labgrid-exporter.""" + +import pytest + +from labgrid.remote.exporter import SerialPortExport + + +@pytest.fixture +def trace_args(monkeypatch): + """Return a builder bound to whatever LG_SERIAL_TRACE_DIR is set.""" + + def _build(group_name="bbb", user="okaro/sjg", path="/dev/ttyUSB0"): + return SerialPortExport._build_trace_args(group_name, user, path) + + return _build + + +def test_returns_empty_when_env_unset(trace_args, monkeypatch): + monkeypatch.delenv("LG_SERIAL_TRACE_DIR", raising=False) + assert trace_args() == [] + + +def test_builds_yaml_args(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + args = trace_args(group_name="bbb", user="okaro/sjg") + assert args == [ + "-Y", + f" trace-both: {tmp_path}/bbb-okaro_sjg.log", + "-Y", + " trace-both-timestamp: true", + ] + + +def test_creates_missing_directory(trace_args, monkeypatch, tmp_path): + target = tmp_path / "newdir" + assert not target.exists() + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(target)) + trace_args() + assert target.is_dir() + + +def test_user_slashes_rewritten(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + args = trace_args(user="myhost/alice") + assert args[1] == f" trace-both: {tmp_path}/bbb-myhost_alice.log" + + +def test_unknown_user_when_none(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + args = trace_args(user=None) + assert args[1] == f" trace-both: {tmp_path}/bbb-unknown.log" + + +def test_falls_back_to_path_basename(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + args = trace_args(group_name="", path="/dev/ttyUSB7") + assert args[1] == f" trace-both: {tmp_path}/ttyUSB7-okaro_sjg.log" + + +def test_existing_dir_is_reused(trace_args, monkeypatch, tmp_path): + """If the dir already exists, makedirs(exist_ok=True) must not fail.""" + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + trace_args() + trace_args() # second call would error if exist_ok wasn't honoured + + +def test_hexdump_disabled_by_default(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + monkeypatch.delenv("LG_SERIAL_TRACE_HEXDUMP", raising=False) + args = trace_args() + assert " trace-both-hexdump: true" not in args + + +def test_hexdump_enabled_when_set(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + monkeypatch.setenv("LG_SERIAL_TRACE_HEXDUMP", "1") + args = trace_args() + assert args[-2:] == ["-Y", " trace-both-hexdump: true"] + + +def test_hexdump_not_enabled_when_other_value(trace_args, monkeypatch, tmp_path): + monkeypatch.setenv("LG_SERIAL_TRACE_DIR", str(tmp_path)) + monkeypatch.setenv("LG_SERIAL_TRACE_HEXDUMP", "true") + args = trace_args() + assert " trace-both-hexdump: true" not in args