diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 069240b9e..50150d50d 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -32,6 +32,11 @@ # TODO: drop if Python >= 3.11 guaranteed from exceptiongroup import ExceptionGroup # pylint: disable=redefined-builtin +from labgrid.remote.grpc.interceptor.client import ( + IdentityClientStreamStreamInterceptor, + IdentityClientUnaryUnaryInterceptor, +) + from .common import ( ResourceEntry, ResourceMatch, @@ -120,9 +125,19 @@ def __attrs_post_init__(self): ("grpc.http2.max_pings_without_data", 0), # no limit ] + identity = { + "username": self.getuser(), + "hostname": self.gethostname(), + "user_agent": f"labgrid-client {labgrid_version()}", + } + interceptors = [ + IdentityClientUnaryUnaryInterceptor(**identity), + IdentityClientStreamStreamInterceptor(**identity), + ] self.channel = grpc.aio.insecure_channel( target=self.address, options=channel_options, + interceptors=interceptors, ) self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel) diff --git a/labgrid/remote/common.py b/labgrid/remote/common.py index 14c8a2d74..3998733f3 100644 --- a/labgrid/remote/common.py +++ b/labgrid/remote/common.py @@ -7,6 +7,8 @@ import logging from datetime import datetime from fnmatch import fnmatchcase +from typing import Optional +import warnings import attr @@ -481,6 +483,21 @@ def from_pb2(cls, pb2: labgrid_coordinator_pb2.Reservation): ) +def get_metadata_single_value_by_key(metadata, key: str) -> Optional[str]: + """Look up a single value by key in a metadata sequence of (key, value) pairs.""" + values = [v for k, v in metadata or () if k == key] + + if not values: + return None + + if len(values) > 1: + warnings.warn( + "Multiple metadata KV pairs with the same key. The value of the first matching KV pair will be returned." + ) + + return values[0] + + async def queue_as_aiter(q): try: while True: diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index 3423f3cef..03d96112b 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import contextvars import logging import asyncio import traceback @@ -10,11 +11,15 @@ import copy import random import signal +from typing import Optional import attr import grpc from grpc_reflection.v1alpha import reflection +from labgrid.remote.grpc.interceptor.server import IdentityServerInterceptor +from labgrid.remote.identity import ClientIdentity, infer_peer_identity + from .common import ( ResourceEntry, ResourceMatch, @@ -30,6 +35,10 @@ from .generated import labgrid_coordinator_pb2_grpc from ..util import atomic_replace, labgrid_version, yaml, Timeout +client_identity_context: contextvars.ContextVar[Optional[ClientIdentity]] = contextvars.ContextVar( + "client_identity", default=None +) + @contextmanager def warn_if_slow(prefix, *, level=logging.WARNING, limit=0.1): @@ -317,9 +326,16 @@ async def ClientStream(self, request_iterator, context): assert peer not in self.clients out_msg_queue = asyncio.Queue() + identity = client_identity_context.get() + if identity: + logging.debug("client identity provided in gRPC metadata") + logging.debug(identity) + self.clients[peer] = ClientSession(self, peer, identity.id, out_msg_queue, identity.user_agent) + async def request_task(): name = None version = None + session = self.clients.get(peer) try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ClientInMessage @@ -330,8 +346,18 @@ async def request_task(): out_msg.sync.id = in_msg.sync.id out_msg_queue.put_nowait(out_msg) elif kind == "startup": - version = in_msg.startup.version + if identity: + logging.debug("ignoring legacy startup message; session initialised from metadata") + continue + if session: + logging.warning("ignoring duplicate startup message from client %s", peer) + continue name = in_msg.startup.name + version = in_msg.startup.version + logging.warning( + "client %s did not provide identity metadata; using deprecated startup identity", + peer, + ) session = self.clients[peer] = ClientSession(self, peer, name, out_msg_queue, version) logging.debug("Received startup from %s with %s", name, version) asyncio.current_task().set_name(f"client-{peer}-rx/started-{name}") @@ -409,9 +435,22 @@ async def ExporterStream(self, request_iterator, context): out_msg.hello.version = labgrid_version() yield out_msg + identity = client_identity_context.get() + if identity: + logging.debug("exporter identity provided in gRPC metadata") + logging.debug(identity) + if existing := self.get_exporter_by_name(identity.id): + await context.abort( + grpc.StatusCode.ALREADY_EXISTS, + f"startup failed: exporter with name '{identity.id}' is already connected from {existing.peer}", + ) + self.exporters[peer] = ExporterSession(self, peer, identity.id, command_queue, identity.user_agent) + startup_done.set() + async def request_task(): name = None version = None + session = self.exporters.get(peer) try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ExporterInMessage @@ -422,8 +461,18 @@ async def request_task(): cmd.complete(in_msg.response) logging.debug("Command %s is done", cmd) elif kind == "startup": - version = in_msg.startup.version + if identity: + logging.debug("ignoring legacy startup message; session initialized from metadata") + continue + if session: + logging.warning("ignoring duplicate startup message from exporter %s", peer) + continue name = in_msg.startup.name + version = in_msg.startup.version + logging.warning( + "exporter %s did not provide identity metadata; using deprecated startup identity", + peer, + ) if existing := self.get_exporter_by_name(name): raise ExporterError( f"exporter with name '{name}' is already connected from {existing.peer}" @@ -852,7 +901,7 @@ async def AcquirePlace(self, request, context): peer = context.peer() name = request.placename try: - username = self.clients[peer].name + username = infer_peer_identity(self.clients, context, client_identity_context) except KeyError: await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"Peer {peer} does not have a valid session") print(request) @@ -922,7 +971,7 @@ async def AllowPlace(self, request, context): user = request.user peer = context.peer() try: - username = self.clients[peer].name + username = infer_peer_identity(self.clients, context, client_identity_context) except KeyError: await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"Peer {peer} does not have a valid session") try: @@ -1077,7 +1126,10 @@ async def CreateReservation(self, request: labgrid_coordinator_pb2.CreateReserva await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Value {v} is invalid") fltr[k] = v - owner = self.clients[peer].name + try: + owner = infer_peer_identity(self.clients, context, client_identity_context) + except KeyError: + await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"Peer {peer} does not have a valid session") res = Reservation(owner=owner, prio=request.prio, filters=fltrs) self.reservations[res.token] = res self.schedule_reservations() @@ -1127,6 +1179,7 @@ async def serve(listen, cleanup) -> None: ] server = grpc.aio.server( options=channel_options, + interceptors=[IdentityServerInterceptor(client_identity_context)], ) coordinator = Coordinator() labgrid_coordinator_pb2_grpc.add_CoordinatorServicer_to_server(coordinator, server) diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index 82f1da6b6..5cf149803 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -20,6 +20,11 @@ import attr import grpc +from labgrid.remote.grpc.interceptor.client import ( + IdentityClientStreamStreamInterceptor, + IdentityClientUnaryUnaryInterceptor, +) + from .config import ResourceConfig from .common import ResourceEntry, queue_as_aiter from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc @@ -834,9 +839,14 @@ def __init__(self, config) -> None: if urlsplit(f"//{config['coordinator']}").port is None: config["coordinator"] += ":20408" + identity = (None, self.name, f"labgrid-exporter {labgrid_version()}") self.channel = grpc.aio.insecure_channel( target=config["coordinator"], options=channel_options, + interceptors=[ + IdentityClientUnaryUnaryInterceptor(*identity), + IdentityClientStreamStreamInterceptor(*identity), + ], ) self.stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(self.channel) self.out_queue = asyncio.Queue() diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2.py b/labgrid/remote/generated/labgrid_coordinator_pb2.py index 37652bff7..6c75474f7 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2.py +++ b/labgrid/remote/generated/labgrid_coordinator_pb2.py @@ -14,13 +14,19 @@ -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\"\x8e\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.StartupDoneB\x02\x18\x01H\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\"0\n\x0bStartupDone\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t:\x02\x18\x01\"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\"\x9e\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.StartupDoneB\x02\x18\x01H\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') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'labgrid_coordinator_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None + _globals['_CLIENTINMESSAGE'].fields_by_name['startup']._options = None + _globals['_CLIENTINMESSAGE'].fields_by_name['startup']._serialized_options = b'\030\001' + _globals['_STARTUPDONE']._options = None + _globals['_STARTUPDONE']._serialized_options = b'\030\001' + _globals['_EXPORTERINMESSAGE'].fields_by_name['startup']._options = None + _globals['_EXPORTERINMESSAGE'].fields_by_name['startup']._serialized_options = b'\030\001' _globals['_RESOURCE_PARAMSENTRY']._options = None _globals['_RESOURCE_PARAMSENTRY']._serialized_options = b'8\001' _globals['_RESOURCE_EXTRAENTRY']._options = None @@ -38,121 +44,121 @@ _globals['_RESERVATION_ALLOCATIONSENTRY']._options = None _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_options = b'8\001' _globals['_CLIENTINMESSAGE']._serialized_start=39 - _globals['_CLIENTINMESSAGE']._serialized_end=177 - _globals['_SYNC']._serialized_start=179 - _globals['_SYNC']._serialized_end=197 - _globals['_STARTUPDONE']._serialized_start=199 - _globals['_STARTUPDONE']._serialized_end=243 - _globals['_SUBSCRIBE']._serialized_start=245 - _globals['_SUBSCRIBE']._serialized_end=359 - _globals['_CLIENTOUTMESSAGE']._serialized_start=361 - _globals['_CLIENTOUTMESSAGE']._serialized_end=464 - _globals['_UPDATERESPONSE']._serialized_start=467 - _globals['_UPDATERESPONSE']._serialized_end=632 - _globals['_EXPORTERINMESSAGE']._serialized_start=635 - _globals['_EXPORTERINMESSAGE']._serialized_end=789 - _globals['_RESOURCE']._serialized_start=792 - _globals['_RESOURCE']._serialized_end=1206 - _globals['_RESOURCE_PATH']._serialized_start=980 - _globals['_RESOURCE_PATH']._serialized_end=1075 - _globals['_RESOURCE_PARAMSENTRY']._serialized_start=1077 - _globals['_RESOURCE_PARAMSENTRY']._serialized_end=1141 - _globals['_RESOURCE_EXTRAENTRY']._serialized_start=1143 - _globals['_RESOURCE_EXTRAENTRY']._serialized_end=1206 - _globals['_MAPVALUE']._serialized_start=1209 - _globals['_MAPVALUE']._serialized_end=1339 - _globals['_EXPORTERRESPONSE']._serialized_start=1341 - _globals['_EXPORTERRESPONSE']._serialized_end=1408 - _globals['_HELLO']._serialized_start=1410 - _globals['_HELLO']._serialized_end=1434 - _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['_CLIENTINMESSAGE']._serialized_end=181 + _globals['_SYNC']._serialized_start=183 + _globals['_SYNC']._serialized_end=201 + _globals['_STARTUPDONE']._serialized_start=203 + _globals['_STARTUPDONE']._serialized_end=251 + _globals['_SUBSCRIBE']._serialized_start=253 + _globals['_SUBSCRIBE']._serialized_end=367 + _globals['_CLIENTOUTMESSAGE']._serialized_start=369 + _globals['_CLIENTOUTMESSAGE']._serialized_end=472 + _globals['_UPDATERESPONSE']._serialized_start=475 + _globals['_UPDATERESPONSE']._serialized_end=640 + _globals['_EXPORTERINMESSAGE']._serialized_start=643 + _globals['_EXPORTERINMESSAGE']._serialized_end=801 + _globals['_RESOURCE']._serialized_start=804 + _globals['_RESOURCE']._serialized_end=1218 + _globals['_RESOURCE_PATH']._serialized_start=992 + _globals['_RESOURCE_PATH']._serialized_end=1087 + _globals['_RESOURCE_PARAMSENTRY']._serialized_start=1089 + _globals['_RESOURCE_PARAMSENTRY']._serialized_end=1153 + _globals['_RESOURCE_EXTRAENTRY']._serialized_start=1155 + _globals['_RESOURCE_EXTRAENTRY']._serialized_end=1218 + _globals['_MAPVALUE']._serialized_start=1221 + _globals['_MAPVALUE']._serialized_end=1351 + _globals['_EXPORTERRESPONSE']._serialized_start=1353 + _globals['_EXPORTERRESPONSE']._serialized_end=1420 + _globals['_HELLO']._serialized_start=1422 + _globals['_HELLO']._serialized_end=1446 + _globals['_EXPORTEROUTMESSAGE']._serialized_start=1449 + _globals['_EXPORTEROUTMESSAGE']._serialized_end=1579 + _globals['_EXPORTERSETACQUIREDREQUEST']._serialized_start=1581 + _globals['_EXPORTERSETACQUIREDREQUEST']._serialized_end=1692 + _globals['_ADDPLACEREQUEST']._serialized_start=1694 + _globals['_ADDPLACEREQUEST']._serialized_end=1725 + _globals['_ADDPLACERESPONSE']._serialized_start=1727 + _globals['_ADDPLACERESPONSE']._serialized_end=1745 + _globals['_DELETEPLACEREQUEST']._serialized_start=1747 + _globals['_DELETEPLACEREQUEST']._serialized_end=1781 + _globals['_DELETEPLACERESPONSE']._serialized_start=1783 + _globals['_DELETEPLACERESPONSE']._serialized_end=1804 + _globals['_GETPLACESREQUEST']._serialized_start=1806 + _globals['_GETPLACESREQUEST']._serialized_end=1824 + _globals['_GETPLACESRESPONSE']._serialized_start=1826 + _globals['_GETPLACESRESPONSE']._serialized_end=1877 + _globals['_PLACE']._serialized_start=1880 + _globals['_PLACE']._serialized_end=2218 + _globals['_PLACE_TAGSENTRY']._serialized_start=2146 + _globals['_PLACE_TAGSENTRY']._serialized_end=2189 + _globals['_RESOURCEMATCH']._serialized_start=2220 + _globals['_RESOURCEMATCH']._serialized_end=2341 + _globals['_ADDPLACEALIASREQUEST']._serialized_start=2343 + _globals['_ADDPLACEALIASREQUEST']._serialized_end=2399 + _globals['_ADDPLACEALIASRESPONSE']._serialized_start=2401 + _globals['_ADDPLACEALIASRESPONSE']._serialized_end=2424 + _globals['_DELETEPLACEALIASREQUEST']._serialized_start=2426 + _globals['_DELETEPLACEALIASREQUEST']._serialized_end=2485 + _globals['_DELETEPLACEALIASRESPONSE']._serialized_start=2487 + _globals['_DELETEPLACEALIASRESPONSE']._serialized_end=2513 + _globals['_SETPLACETAGSREQUEST']._serialized_start=2516 + _globals['_SETPLACETAGSREQUEST']._serialized_end=2655 + _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_start=2146 + _globals['_SETPLACETAGSREQUEST_TAGSENTRY']._serialized_end=2189 + _globals['_SETPLACETAGSRESPONSE']._serialized_start=2657 + _globals['_SETPLACETAGSRESPONSE']._serialized_end=2679 + _globals['_SETPLACECOMMENTREQUEST']._serialized_start=2681 + _globals['_SETPLACECOMMENTREQUEST']._serialized_end=2741 + _globals['_SETPLACECOMMENTRESPONSE']._serialized_start=2743 + _globals['_SETPLACECOMMENTRESPONSE']._serialized_end=2768 + _globals['_ADDPLACEMATCHREQUEST']._serialized_start=2770 + _globals['_ADDPLACEMATCHREQUEST']._serialized_end=2860 + _globals['_ADDPLACEMATCHRESPONSE']._serialized_start=2862 + _globals['_ADDPLACEMATCHRESPONSE']._serialized_end=2885 + _globals['_DELETEPLACEMATCHREQUEST']._serialized_start=2887 + _globals['_DELETEPLACEMATCHREQUEST']._serialized_end=2980 + _globals['_DELETEPLACEMATCHRESPONSE']._serialized_start=2982 + _globals['_DELETEPLACEMATCHRESPONSE']._serialized_end=3008 + _globals['_ACQUIREPLACEREQUEST']._serialized_start=3010 + _globals['_ACQUIREPLACEREQUEST']._serialized_end=3050 + _globals['_ACQUIREPLACERESPONSE']._serialized_start=3052 + _globals['_ACQUIREPLACERESPONSE']._serialized_end=3074 + _globals['_RELEASEPLACEREQUEST']._serialized_start=3076 + _globals['_RELEASEPLACEREQUEST']._serialized_end=3152 + _globals['_RELEASEPLACERESPONSE']._serialized_start=3154 + _globals['_RELEASEPLACERESPONSE']._serialized_end=3176 + _globals['_ALLOWPLACEREQUEST']._serialized_start=3178 + _globals['_ALLOWPLACEREQUEST']._serialized_end=3230 + _globals['_ALLOWPLACERESPONSE']._serialized_start=3232 + _globals['_ALLOWPLACERESPONSE']._serialized_end=3252 + _globals['_CREATERESERVATIONREQUEST']._serialized_start=3255 + _globals['_CREATERESERVATIONREQUEST']._serialized_end=3437 + _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_start=3362 + _globals['_CREATERESERVATIONREQUEST_FILTERSENTRY']._serialized_end=3437 + _globals['_CREATERESERVATIONRESPONSE']._serialized_start=3439 + _globals['_CREATERESERVATIONRESPONSE']._serialized_end=3509 + _globals['_RESERVATION']._serialized_start=3512 + _globals['_RESERVATION']._serialized_end=3973 + _globals['_RESERVATION_FILTER']._serialized_start=3732 + _globals['_RESERVATION_FILTER']._serialized_end=3844 + _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_start=3799 + _globals['_RESERVATION_FILTER_FILTERENTRY']._serialized_end=3844 + _globals['_RESERVATION_FILTERSENTRY']._serialized_start=3362 + _globals['_RESERVATION_FILTERSENTRY']._serialized_end=3437 + _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_start=3923 + _globals['_RESERVATION_ALLOCATIONSENTRY']._serialized_end=3973 + _globals['_CANCELRESERVATIONREQUEST']._serialized_start=3975 + _globals['_CANCELRESERVATIONREQUEST']._serialized_end=4016 + _globals['_CANCELRESERVATIONRESPONSE']._serialized_start=4018 + _globals['_CANCELRESERVATIONRESPONSE']._serialized_end=4045 + _globals['_POLLRESERVATIONREQUEST']._serialized_start=4047 + _globals['_POLLRESERVATIONREQUEST']._serialized_end=4086 + _globals['_POLLRESERVATIONRESPONSE']._serialized_start=4088 + _globals['_POLLRESERVATIONRESPONSE']._serialized_end=4156 + _globals['_GETRESERVATIONSRESPONSE']._serialized_start=4158 + _globals['_GETRESERVATIONSRESPONSE']._serialized_end=4227 + _globals['_GETRESERVATIONSREQUEST']._serialized_start=4229 + _globals['_GETRESERVATIONSREQUEST']._serialized_end=4253 + _globals['_COORDINATOR']._serialized_start=4256 + _globals['_COORDINATOR']._serialized_end=5746 # @@protoc_insertion_point(module_scope) diff --git a/labgrid/remote/grpc/__init__.py b/labgrid/remote/grpc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/labgrid/remote/grpc/interceptor/__init__.py b/labgrid/remote/grpc/interceptor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/labgrid/remote/grpc/interceptor/client.py b/labgrid/remote/grpc/interceptor/client.py new file mode 100644 index 000000000..478c6dd63 --- /dev/null +++ b/labgrid/remote/grpc/interceptor/client.py @@ -0,0 +1,32 @@ +from typing import Optional + +from grpc.aio import ClientCallDetails, ClientInterceptor, StreamStreamClientInterceptor, UnaryUnaryClientInterceptor + +from labgrid.remote.identity import HOSTNAME_KEY, USER_AGENT_KEY, USERNAME_KEY + + +class BaseIdentityClientInterceptor(ClientInterceptor): + def __init__(self, username: Optional[str], hostname: str, user_agent: Optional[str]): + super().__init__() + self.username = username + self.hostname = hostname + self.user_agent = user_agent + + def _inject(self, client_call_details: ClientCallDetails): + if self.username: + client_call_details.metadata.add(USERNAME_KEY, self.username) + client_call_details.metadata.add(HOSTNAME_KEY, self.hostname) + if self.user_agent: + client_call_details.metadata.add(USER_AGENT_KEY, self.user_agent) + + +class IdentityClientUnaryUnaryInterceptor(UnaryUnaryClientInterceptor, BaseIdentityClientInterceptor): + async def intercept_unary_unary(self, continuation, client_call_details, request): + self._inject(client_call_details) + return await continuation(client_call_details, request) + + +class IdentityClientStreamStreamInterceptor(StreamStreamClientInterceptor, BaseIdentityClientInterceptor): + async def intercept_stream_stream(self, continuation, client_call_details, request_iterator): + self._inject(client_call_details) + return await continuation(client_call_details, request_iterator) diff --git a/labgrid/remote/grpc/interceptor/server.py b/labgrid/remote/grpc/interceptor/server.py new file mode 100644 index 000000000..3727fced6 --- /dev/null +++ b/labgrid/remote/grpc/interceptor/server.py @@ -0,0 +1,33 @@ +import contextvars +import logging +from asyncio import iscoroutine + +from grpc.aio import ServerInterceptor + +from labgrid.remote.identity import ClientIdentity, NoIdentityPresent + + +class IdentityServerInterceptor(ServerInterceptor): + def __init__(self, client_identity_contextvar: contextvars.ContextVar): + super().__init__() + self.client_identity_contextvar = client_identity_contextvar + + async def intercept_service(self, continuation, handler_call_details): + # continuation may return a handler + # OR an awaitable depending on grpcio build + maybe_handler = continuation(handler_call_details) + handler = await maybe_handler if iscoroutine(maybe_handler) else maybe_handler + if handler is None: + return None + + metadata = handler_call_details.invocation_metadata + logging.debug(metadata) + + try: + client_identity = ClientIdentity.from_metadata(metadata) + logging.debug(client_identity) + self.client_identity_contextvar.set(client_identity) + except NoIdentityPresent: + pass + + return handler diff --git a/labgrid/remote/identity.py b/labgrid/remote/identity.py new file mode 100644 index 000000000..442fc755d --- /dev/null +++ b/labgrid/remote/identity.py @@ -0,0 +1,65 @@ +import contextvars +import logging +from typing import Optional + +from labgrid.remote.common import get_metadata_single_value_by_key + +USERNAME_KEY = "x-lg-username" +HOSTNAME_KEY = "x-lg-hostname" +USER_AGENT_KEY = "x-lg-user-agent" + + +class NoIdentityPresent(Exception): + """Raised when metadata-based identity information is missing from the request.""" + + +class ClientIdentity: + """Represents the identity of a connected client, derived from gRPC metadata.""" + + def __init__(self, identity_id: str, user_agent: Optional[str]): + self.id = identity_id + self.user_agent = user_agent + + def __str__(self): + return f"ClientIdentity(id={self.id}, user_agent={self.user_agent})" + + @classmethod + def from_metadata(cls, metadata: tuple): + """Construct a ClientIdentity from gRPC request metadata. + + Args: + metadata: A sequence of (key, value) pairs from the gRPC context. + + Returns: + A ClientIdentity with id set to ``hostname/username`` (or just + ``hostname`` if no username is present) and (optional) user_agent. + + Raises: + NoIdentityPresent: If the hostname key is missing from metadata. + """ + username = get_metadata_single_value_by_key(metadata, USERNAME_KEY) + hostname = get_metadata_single_value_by_key(metadata, HOSTNAME_KEY) + user_agent = get_metadata_single_value_by_key(metadata, USER_AGENT_KEY) + + if not hostname: + raise NoIdentityPresent() + + if username: + return cls(f"{hostname}/{username}", user_agent) + + return cls(hostname, user_agent) + + +def infer_peer_identity(clients, context, identity_contextvar: contextvars.ContextVar[Optional[ClientIdentity]]): + logger = logging.getLogger("infer_peer_identity") + + user = identity_contextvar.get() + if user: + logger.debug("identity sourced from metadata") + return user.id + + try: + logger.debug("identity sourced from self.clients") + return clients[context.peer()].name + except KeyError: + raise diff --git a/labgrid/remote/proto/labgrid-coordinator.proto b/labgrid/remote/proto/labgrid-coordinator.proto index e0585f7e1..8d633b160 100644 --- a/labgrid/remote/proto/labgrid-coordinator.proto +++ b/labgrid/remote/proto/labgrid-coordinator.proto @@ -43,7 +43,7 @@ service Coordinator { message ClientInMessage { oneof kind { Sync sync = 1; - StartupDone startup = 2; + StartupDone startup = 2 [deprecated = true]; Subscribe subscribe = 3; }; }; @@ -53,6 +53,8 @@ message Sync { }; message StartupDone { + option deprecated = true; + string version = 1; string name = 2; }; @@ -82,7 +84,7 @@ message UpdateResponse { message ExporterInMessage { oneof kind { Resource resource = 1; - StartupDone startup = 2; + StartupDone startup = 2 [deprecated = true]; ExporterResponse response = 3; }; }; diff --git a/pyproject.toml b/pyproject.toml index 499d1f087..153640e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ dev = [ # additional dev dependencies "psutil>=5.8.0", + "pytest-asyncio==1.3.0", "pytest-benchmark>=4.0.0", "pytest-cov>=3.0.0", "pytest-dependency>=0.5.1", @@ -121,6 +122,8 @@ packages = [ "labgrid.pytestplugin", "labgrid.remote", "labgrid.remote.generated", + "labgrid.remote.grpc", + "labgrid.remote.grpc.interceptor", "labgrid.resource", "labgrid.strategy", "labgrid.util", diff --git a/tests/test_interceptor_client.py b/tests/test_interceptor_client.py new file mode 100644 index 000000000..eb8623618 --- /dev/null +++ b/tests/test_interceptor_client.py @@ -0,0 +1,73 @@ +import pytest + +from labgrid.remote.grpc.interceptor.client import ( + BaseIdentityClientInterceptor, + IdentityClientUnaryUnaryInterceptor, + IdentityClientStreamStreamInterceptor, +) +from labgrid.remote.identity import USERNAME_KEY, HOSTNAME_KEY, USER_AGENT_KEY + + +class DummyMetadata: + def __init__(self): + self.items = [] + + def add(self, key, value): + self.items.append((key, value)) + + +class DummyClientCallDetails: + def __init__(self): + self.metadata = DummyMetadata() + + +def test_base_identity_client_interceptor_injects_all_fields(): + interceptor = BaseIdentityClientInterceptor( + username="test_username", + hostname="test_hostname", + user_agent="test_agent", + ) + + client_call_details = DummyClientCallDetails() + + interceptor._inject(client_call_details) + + assert client_call_details.metadata.items == [ + (USERNAME_KEY, "test_username"), + (HOSTNAME_KEY, "test_hostname"), + (USER_AGENT_KEY, "test_agent"), + ] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "impl,method", + [ + (IdentityClientUnaryUnaryInterceptor, "intercept_unary_unary"), + (IdentityClientStreamStreamInterceptor, "intercept_stream_stream"), + ], +) +async def test_client_interceptor_implementations(impl, method): + interceptor = impl("test_username", "test_hostname", "test_agent") + + client_call_details = DummyClientCallDetails() + request_or_iterator = object() + sentinel_response = object() + + received = {} + + async def continuation(ccd, req): + received["ccd"] = ccd + received["req"] = req + return sentinel_response + + interceptor_method = getattr(interceptor, method) + result = await interceptor_method(continuation, client_call_details, request_or_iterator) + + assert result is sentinel_response + assert received["ccd"] is client_call_details + assert client_call_details.metadata.items == [ + (USERNAME_KEY, "test_username"), + (HOSTNAME_KEY, "test_hostname"), + (USER_AGENT_KEY, "test_agent"), + ] diff --git a/tests/test_interceptor_server.py b/tests/test_interceptor_server.py new file mode 100644 index 000000000..0b9741712 --- /dev/null +++ b/tests/test_interceptor_server.py @@ -0,0 +1,33 @@ +import contextvars, pytest +from types import SimpleNamespace +from unittest.mock import Mock +from labgrid.remote.grpc.interceptor.server import IdentityServerInterceptor + + +@pytest.fixture +def cv(): + return contextvars.ContextVar("client_identity", default=None) + + +@pytest.fixture +def interceptor(cv): + return IdentityServerInterceptor(cv) + + +def handler_call_details(metadata): + return SimpleNamespace(invocation_metadata=tuple(metadata)) + + +@pytest.mark.asyncio +async def test_server_interceptor_sets_contextvar(interceptor, cv): + handler = object() + continuation = Mock(return_value=handler) + + metadata = (("x-lg-hostname", "h"), ("x-lg-username", "u"), ("x-lg-user-agent", "ua")) + + ret = await interceptor.intercept_service(continuation, handler_call_details(metadata)) + assert ret is handler + + identity = cv.get() + assert identity.id == "h/u" + assert identity.user_agent == "ua" diff --git a/tests/test_remote.py b/tests/test_remote.py index 76a1da434..76021c63f 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,4 +1,9 @@ +import warnings + import pexpect +import pytest + +from labgrid.remote.common import get_metadata_single_value_by_key def test_client_help(): @@ -48,3 +53,50 @@ def test_exporter_coordinator_becomes_unreachable(coordinator, exporter): assert exporter.exitstatus == 100 coordinator.resume_tree() + + +def test_get_metadata_single_value_by_key_returns_value_for_existing_key(): + metadata = [("key1", "value1"), ("key2", "value2")] + assert get_metadata_single_value_by_key(metadata, "key1") == "value1" + assert get_metadata_single_value_by_key(metadata, "key2") == "value2" + + +def test_get_metadata_single_value_by_key_returns_none_for_missing_key(): + metadata = [("key1", "value1")] + assert get_metadata_single_value_by_key(metadata, "other") is None + + +def test_get_metadata_single_value_by_key_returns_none_for_empty_metadata(): + assert get_metadata_single_value_by_key((), "key") is None + + +def test_get_metadata_single_value_by_key_returns_none_for_none_metadata(): + assert get_metadata_single_value_by_key(None, "key") is None + + +def test_get_metadata_single_value_by_key_returns_first_value_on_duplicate_keys(): + metadata = [("key", "first"), ("key", "second")] + with pytest.warns(UserWarning, match="Multiple metadata KV pairs"): + result = get_metadata_single_value_by_key(metadata, "key") + assert result == "first" + + +def test_get_metadata_single_value_by_key_no_warning_on_single_match(): + metadata = [("key", "value")] + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + get_metadata_single_value_by_key(metadata, "key") + assert len(caught) == 0 + + +def test_get_metadata_single_value_by_key_returns_first_value_for_non_adjacent_duplicates(): + metadata = [("key", "first"), ("other", "value"), ("key", "second")] + with pytest.warns(UserWarning, match="Multiple metadata KV pairs"): + result = get_metadata_single_value_by_key(metadata, "key") + assert result == "first" + + +def test_get_metadata_single_value_by_key_is_case_sensitive(): + metadata = [("Key", "value")] + assert get_metadata_single_value_by_key(metadata, "key") is None + assert get_metadata_single_value_by_key(metadata, "Key") == "value"