From 92b027bb079c4f9f808ce651cfb641ec728d4463 Mon Sep 17 00:00:00 2001 From: Asher Pemberton Date: Thu, 9 Apr 2026 15:02:47 +0100 Subject: [PATCH 1/8] remote: add metadata-based gRPC client identity Add client and server interceptors which attach labgrid identity metadata to gRPC calls and expose it to coordinator RPC handlers. Use the metadata identity to register client and exporter stream sessions while keeping startup-message handling as a deprecated fallback for older clients and exporters. Signed-off-by: Asher Pemberton Reviewed-by: Asher Pemberton # gatekeeper Co-authored-by: Luke Beardsmore --- labgrid/remote/client.py | 15 +++++ labgrid/remote/common.py | 17 +++++ labgrid/remote/coordinator.py | 10 +++ labgrid/remote/exporter.py | 10 +++ labgrid/remote/grpc/__init__.py | 0 labgrid/remote/grpc/interceptor/__init__.py | 0 labgrid/remote/grpc/interceptor/client.py | 32 +++++++++ labgrid/remote/grpc/interceptor/server.py | 33 ++++++++++ labgrid/remote/identity.py | 48 ++++++++++++++ pyproject.toml | 3 + tests/test_interceptor_client.py | 73 +++++++++++++++++++++ tests/test_interceptor_server.py | 33 ++++++++++ tests/test_remote.py | 52 +++++++++++++++ 13 files changed, 326 insertions(+) create mode 100644 labgrid/remote/grpc/__init__.py create mode 100644 labgrid/remote/grpc/interceptor/__init__.py create mode 100644 labgrid/remote/grpc/interceptor/client.py create mode 100644 labgrid/remote/grpc/interceptor/server.py create mode 100644 labgrid/remote/identity.py create mode 100644 tests/test_interceptor_client.py create mode 100644 tests/test_interceptor_server.py 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..dae1a3d34 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 + 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): @@ -1127,6 +1136,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/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..535373ee4 --- /dev/null +++ b/labgrid/remote/identity.py @@ -0,0 +1,48 @@ +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) diff --git a/pyproject.toml b/pyproject.toml index a046ebe65..bd964d48e 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", @@ -120,6 +121,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" From 4ea295125886f1705659519e82a3a0794e3b1abc Mon Sep 17 00:00:00 2001 From: Asher Pemberton Date: Fri, 10 Apr 2026 09:21:44 +0100 Subject: [PATCH 2/8] remote/coordinator: detach place RPCs from ClientStream Allow AcquirePlace, ReleasePlace and CreateReservation to identify the caller from gRPC metadata instead of requiring identity to come only from an established ClientStream session. Keep the existing ClientStream session lookup as a fallback so older clients which still send startup messages on the stream continue to work. Signed-off-by: Asher Pemberton Reviewed-by: Asher Pemberton # gatekeeper Co-authored-by: Luke Beardsmore --- labgrid/remote/client.py | 4 - labgrid/remote/coordinator.py | 39 ++- labgrid/remote/exporter.py | 7 - .../generated/labgrid_coordinator_pb2.py | 242 +++++++++--------- labgrid/remote/identity.py | 17 ++ .../remote/proto/labgrid-coordinator.proto | 6 +- 6 files changed, 180 insertions(+), 135 deletions(-) diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 50150d50d..7b7a7bc9b 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -154,10 +154,6 @@ async def start(self): self.pump_task = self.loop.create_task(self.message_pump()) msg = labgrid_coordinator_pb2.ClientInMessage() - msg.startup.version = labgrid_version() - msg.startup.name = f"{self.gethostname()}/{self.getuser()}" - self.out_queue.put_nowait(msg) - msg = labgrid_coordinator_pb2.ClientInMessage() msg.subscribe.all_places = True self.out_queue.put_nowait(msg) msg = labgrid_coordinator_pb2.ClientInMessage() diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index dae1a3d34..8540c0870 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -18,7 +18,7 @@ from grpc_reflection.v1alpha import reflection from labgrid.remote.grpc.interceptor.server import IdentityServerInterceptor -from labgrid.remote.identity import ClientIdentity +from labgrid.remote.identity import ClientIdentity, infer_peer_identity from .common import ( ResourceEntry, @@ -326,9 +326,17 @@ 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 + if peer in self.clients: + session = self.clients[peer] try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ClientInMessage @@ -339,6 +347,9 @@ async def request_task(): out_msg.sync.id = in_msg.sync.id out_msg_queue.put_nowait(out_msg) elif kind == "startup": + if peer in self.clients: + logging.debug("already setup, probably because identity was provided in metadata") + continue version = in_msg.startup.version name = in_msg.startup.name session = self.clients[peer] = ClientSession(self, peer, name, out_msg_queue, version) @@ -418,9 +429,23 @@ 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 + if peer in self.exporters: + session = self.exporters[peer] try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ExporterInMessage @@ -431,6 +456,9 @@ async def request_task(): cmd.complete(in_msg.response) logging.debug("Command %s is done", cmd) elif kind == "startup": + if peer in self.exporters: + logging.debug("already setup, probably because identity was provided in metadata") + continue version = in_msg.startup.version name = in_msg.startup.name if existing := self.get_exporter_by_name(name): @@ -861,7 +889,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) @@ -931,7 +959,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: @@ -1086,7 +1114,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() diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index 5cf149803..1bdb72fc0 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -858,7 +858,6 @@ def __init__(self, config) -> None: async def run(self) -> None: self.pump_task = self.loop.create_task(self.message_pump()) - self.send_started() config_template_env = { "env": os.environ, @@ -905,12 +904,6 @@ async def run(self) -> None: except asyncio.CancelledError: return - def send_started(self): - msg = labgrid_coordinator_pb2.ExporterInMessage() - msg.startup.version = labgrid_version() - msg.startup.name = self.name - self.out_queue.put_nowait(msg) - async def message_pump(self): got_message = False try: 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/identity.py b/labgrid/remote/identity.py index 535373ee4..442fc755d 100644 --- a/labgrid/remote/identity.py +++ b/labgrid/remote/identity.py @@ -1,3 +1,5 @@ +import contextvars +import logging from typing import Optional from labgrid.remote.common import get_metadata_single_value_by_key @@ -46,3 +48,18 @@ def from_metadata(cls, metadata: tuple): 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; }; }; From 04a49f6552d2230ec202106a7b9b86e4820f261a Mon Sep 17 00:00:00 2001 From: Asher Pemberton Date: Tue, 14 Jul 2026 13:40:00 +0100 Subject: [PATCH 3/8] remote: preserve compatibility during identity migration Send StartupDone alongside identity metadata so new clients and exporters remain compatible with older coordinators. Create sessions immediately from identity metadata when available, while retaining StartupDone as a deprecated fallback. Signed-off-by: Asher Pemberton Reviewed-by: Asher Pemberton # gatekeeper --- labgrid/remote/client.py | 4 ++++ labgrid/remote/coordinator.py | 32 ++++++++++++++++++++++---------- labgrid/remote/exporter.py | 7 +++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 7b7a7bc9b..50150d50d 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -154,6 +154,10 @@ async def start(self): self.pump_task = self.loop.create_task(self.message_pump()) msg = labgrid_coordinator_pb2.ClientInMessage() + msg.startup.version = labgrid_version() + msg.startup.name = f"{self.gethostname()}/{self.getuser()}" + self.out_queue.put_nowait(msg) + msg = labgrid_coordinator_pb2.ClientInMessage() msg.subscribe.all_places = True self.out_queue.put_nowait(msg) msg = labgrid_coordinator_pb2.ClientInMessage() diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index 8540c0870..03d96112b 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -335,8 +335,7 @@ async def ClientStream(self, request_iterator, context): async def request_task(): name = None version = None - if peer in self.clients: - session = self.clients[peer] + session = self.clients.get(peer) try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ClientInMessage @@ -347,11 +346,18 @@ async def request_task(): out_msg.sync.id = in_msg.sync.id out_msg_queue.put_nowait(out_msg) elif kind == "startup": - if peer in self.clients: - logging.debug("already setup, probably because identity was provided in metadata") + 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 - version = in_msg.startup.version 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}") @@ -444,8 +450,7 @@ async def ExporterStream(self, request_iterator, context): async def request_task(): name = None version = None - if peer in self.exporters: - session = self.exporters[peer] + session = self.exporters.get(peer) try: async for in_msg in request_iterator: in_msg: labgrid_coordinator_pb2.ExporterInMessage @@ -456,11 +461,18 @@ async def request_task(): cmd.complete(in_msg.response) logging.debug("Command %s is done", cmd) elif kind == "startup": - if peer in self.exporters: - logging.debug("already setup, probably because identity was provided in metadata") + 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 - version = in_msg.startup.version 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}" diff --git a/labgrid/remote/exporter.py b/labgrid/remote/exporter.py index 1bdb72fc0..5cf149803 100755 --- a/labgrid/remote/exporter.py +++ b/labgrid/remote/exporter.py @@ -858,6 +858,7 @@ def __init__(self, config) -> None: async def run(self) -> None: self.pump_task = self.loop.create_task(self.message_pump()) + self.send_started() config_template_env = { "env": os.environ, @@ -904,6 +905,12 @@ async def run(self) -> None: except asyncio.CancelledError: return + def send_started(self): + msg = labgrid_coordinator_pb2.ExporterInMessage() + msg.startup.version = labgrid_version() + msg.startup.name = self.name + self.out_queue.put_nowait(msg) + async def message_pump(self): got_message = False try: From 33637cd425663a73909c924be2b2429c96d98dee Mon Sep 17 00:00:00 2001 From: Rouven Czerwinski Date: Fri, 24 Jul 2026 09:47:55 +0200 Subject: [PATCH 4/8] remote/auth: add initial capabilities Taken verbatim from the authentication github discussion [1]. [1]: https://github.com/labgrid-project/labgrid/discussions/1883 Signed-off-by: Rouven Czerwinski --- labgrid/remote/auth/__init__.py | 0 labgrid/remote/auth/capability.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 labgrid/remote/auth/__init__.py create mode 100644 labgrid/remote/auth/capability.py diff --git a/labgrid/remote/auth/__init__.py b/labgrid/remote/auth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/labgrid/remote/auth/capability.py b/labgrid/remote/auth/capability.py new file mode 100644 index 000000000..5816b0740 --- /dev/null +++ b/labgrid/remote/auth/capability.py @@ -0,0 +1,25 @@ +from enum import StrEnum, auto + + +class Capability(StrEnum): + client_stream = auto() + exporter_stream = auto() + add_place = auto() + delete_place = auto() + get_places = auto() + add_place_alias = auto() + delete_place_alias = auto() + set_place_tags = auto() + set_place_comment = auto() + add_place_match = auto() + delete_place_match = auto() + acquire_place = auto() + release_place_owned = auto() + release_place_any = auto() + allow_place_owned = auto() + allow_place_any = auto() + create_reservation = auto() + cancel_reservation_owned = auto() + cancel_reservation_any = auto() + poll_reservation = auto() + get_reservations = auto() From 851714030ec28eb0e4dd874cb3989765da707459 Mon Sep 17 00:00:00 2001 From: Rouven Czerwinski Date: Fri, 24 Jul 2026 16:04:38 +0200 Subject: [PATCH 5/8] remote/identity: add all capabilities This is a stop-gap until we have policies to assign capabilities. Signed-off-by: Rouven Czerwinski --- labgrid/remote/identity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/labgrid/remote/identity.py b/labgrid/remote/identity.py index 442fc755d..a395d7e8c 100644 --- a/labgrid/remote/identity.py +++ b/labgrid/remote/identity.py @@ -1,6 +1,7 @@ import contextvars import logging from typing import Optional +from .auth.capability import Capability from labgrid.remote.common import get_metadata_single_value_by_key @@ -19,6 +20,7 @@ class ClientIdentity: def __init__(self, identity_id: str, user_agent: Optional[str]): self.id = identity_id self.user_agent = user_agent + self.capabilities = set(e for e in Capability) def __str__(self): return f"ClientIdentity(id={self.id}, user_agent={self.user_agent})" From c52111bcf44a91581324f69cc929490ae54fc865 Mon Sep 17 00:00:00 2001 From: Rouven Czerwinski Date: Fri, 24 Jul 2026 16:05:09 +0200 Subject: [PATCH 6/8] coordinator: implement capabilities Implement capabilities by using a decorator for unary calls and checking permissions manually for stream calls. Capabilities with more scoping (owned vs any) do not use the decorator and instead check manually. With the capabilities checked by the Coordinator, future Labgrid versions will be able to use policies to assign capabilities to specific identities and thus restrict which gRPC calls can be performed on the coordinator. Signed-off-by: Rouven Czerwinski --- labgrid/remote/coordinator.py | 106 ++++++++++++++++++++-- labgrid/remote/grpc/interceptor/server.py | 1 + 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index 03d96112b..f44514949 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -19,6 +19,7 @@ from labgrid.remote.grpc.interceptor.server import IdentityServerInterceptor from labgrid.remote.identity import ClientIdentity, infer_peer_identity +from .auth.capability import Capability from .common import ( ResourceEntry, @@ -190,6 +191,23 @@ async def wrapper(self, *args, **kwargs): return wrapper +async def check_capability(cls, identity, capability, context): + if not identity and cls.use_capabilities == True: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + if identity and (capability not in identity.capabilities): + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {req_cap} not in client capabilities {ctx.capabilities}") + +def require_capability(req_cap): + def decorator(func): + @wraps(func) + async def wrapper(self, request, context): + ctx = client_identity_context.get() + await check_capability(self, ctx, req_cap, context) + return await func(self, request, context) + + return wrapper + return decorator + class ExporterCommand: def __init__(self, request) -> None: @@ -219,7 +237,8 @@ class ExporterError(Exception): class Coordinator(labgrid_coordinator_pb2_grpc.CoordinatorServicer): - def __init__(self) -> None: + def __init__(self, use_capabilities) -> None: + self.use_capabilities = use_capabilities self.places: dict[str, Place] = {} self.reservations = {} self.poll_tasks = [] @@ -327,6 +346,10 @@ async def ClientStream(self, request_iterator, context): out_msg_queue = asyncio.Queue() identity = client_identity_context.get() + print(f"Stream identity: {identity}") + if self.use_capabilities: + await check_capability(self, identity, Capability.client_stream, context) + if identity: logging.debug("client identity provided in gRPC metadata") logging.debug(identity) @@ -426,6 +449,11 @@ async def ExporterStream(self, request_iterator, context): peer = context.peer() logging.info("exporter connected: %s", peer) assert peer not in self.exporters + + identity = client_identity_context.get() + if self.use_capabilities: + await check_capability(self, identity, Capability.exporter_stream, context) + command_queue = asyncio.Queue() pending_commands = [] @@ -548,6 +576,7 @@ async def request_task(): except KeyError: logging.info("Never received startup from peer %s that disconnected", peer) + @require_capability(Capability.add_place) @locked async def AddPlace(self, request, context): name = request.name @@ -562,6 +591,7 @@ async def AddPlace(self, request, context): self.save_later() return labgrid_coordinator_pb2.AddPlaceResponse() + @require_capability(Capability.delete_place) @locked async def DeletePlace(self, request, context): name = request.name @@ -578,6 +608,7 @@ async def DeletePlace(self, request, context): self.save_later() return labgrid_coordinator_pb2.DeletePlaceResponse() + @require_capability(Capability.add_place_alias) @locked async def AddPlaceAlias(self, request, context): placename = request.placename @@ -592,6 +623,7 @@ async def AddPlaceAlias(self, request, context): self.save_later() return labgrid_coordinator_pb2.AddPlaceAliasResponse() + @require_capability(Capability.delete_place_alias) @locked async def DeletePlaceAlias(self, request, context): placename = request.placename @@ -609,6 +641,7 @@ async def DeletePlaceAlias(self, request, context): self.save_later() return labgrid_coordinator_pb2.DeletePlaceAliasResponse() + @require_capability(Capability.set_place_tags) @locked async def SetPlaceTags(self, request, context): placename = request.placename @@ -638,6 +671,7 @@ async def SetPlaceTags(self, request, context): self.save_later() return labgrid_coordinator_pb2.SetPlaceTagsResponse() + @require_capability(Capability.set_place_comment) @locked async def SetPlaceComment(self, request, context): placename = request.placename @@ -652,6 +686,7 @@ async def SetPlaceComment(self, request, context): self.save_later() return labgrid_coordinator_pb2.SetPlaceCommentResponse() + @require_capability(Capability.add_place_match) @locked async def AddPlaceMatch(self, request, context): placename = request.placename @@ -670,6 +705,7 @@ async def AddPlaceMatch(self, request, context): self.save_later() return labgrid_coordinator_pb2.AddPlaceMatchResponse() + @require_capability(Capability.delete_place_match) @locked async def DeletePlaceMatch(self, request, context): placename = request.placename @@ -896,6 +932,7 @@ async def _synchronize_resources(self): idx = place.acquired_resources.index(oldresource) place.acquired_resources[idx] = newresource + @require_capability(Capability.acquire_place) @locked async def AcquirePlace(self, request, context): peer = context.peer() @@ -940,9 +977,16 @@ async def AcquirePlace(self, request, context): @locked async def ReleasePlace(self, request, context): + identity = client_identity_context.get() + if self.use_capabilities and not identity: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + name = request.placename - print(request) fromuser = request.fromuser if request.HasField("fromuser") else None + + if fromuser and self.use_capabilities and not Capability.release_place_any in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}") + try: place = self.places[name] except KeyError: @@ -954,6 +998,22 @@ async def ReleasePlace(self, request, context): if fromuser and place.acquired != fromuser: return labgrid_coordinator_pb2.ReleasePlaceResponse() + try: + 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") + + + if self.use_capabilities: + if not identity: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + + owned = username == place.acquired + if owned and not (Capability.release_place_owned in identity.capabilities or Capability.release_place_any in identity.capabilities): + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_owned} not in client capabilities {identity.capabilities}") + elif not owned and not Capability.release_place_any in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}") + await self._release_resources(place, place.acquired_resources) place.acquired = None @@ -970,6 +1030,9 @@ async def AllowPlace(self, request, context): placename = request.placename user = request.user peer = context.peer() + + identity = client_identity_context.get() + try: username = infer_peer_identity(self.clients, context, client_identity_context) except KeyError: @@ -980,10 +1043,20 @@ async def AllowPlace(self, request, context): await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Place {placename} does not exist") if not place.acquired: await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"Place {placename} is not acquired") - if not place.acquired == username: + owned = place.acquired == username + if not owned and not self.use_capabilities: await context.abort( grpc.StatusCode.FAILED_PRECONDITION, f"Place {placename} is not acquired by {username}" ) + + if self.use_capabilities: + if not identity: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + if owned and not Capability.allow_place_owned in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.allow_place_owned} not in client capabilities {identity.capabilities}") + elif not owned and not Capability.allow_place_any in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.allow_place_any} not in client capabilities {identity.capabilities}") + place.allowed.add(user) place.touch() self._publish_place(place) @@ -993,6 +1066,7 @@ async def AllowPlace(self, request, context): def _get_places(self): return {k: v.asdict() for k, v in self.places.items()} + @require_capability(Capability.get_places) @locked async def GetPlaces(self, unused_request, unused_context): logging.debug("GetPlaces") @@ -1108,6 +1182,7 @@ def schedule_reservations(self): if old_map.get(name) != new_map.get(name): self._publish_place(self.places[name]) + @require_capability(Capability.create_reservation) @locked async def CreateReservation(self, request: labgrid_coordinator_pb2.CreateReservationRequest, context): peer = context.peer() @@ -1137,15 +1212,32 @@ async def CreateReservation(self, request: labgrid_coordinator_pb2.CreateReserva @locked async def CancelReservation(self, request: labgrid_coordinator_pb2.CancelReservationRequest, context): + identity = client_identity_context.get() token = request.token if not isinstance(token, str) or not token: await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Invalid token {token}") if token not in self.reservations: await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"Reservation {token} does not exist") + + 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") + + owned = self.reservations[token].owner == owner + if self.use_capabilities: + if not identity: + await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + if owned and not Capability.cancel_reservation_owned in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}") + elif not owned and not Capability.cancel_reservation_any in identity.capabilities: + await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}") + del self.reservations[token] self.schedule_reservations() return labgrid_coordinator_pb2.CancelReservationResponse() + @require_capability(Capability.poll_reservation) @locked async def PollReservation(self, request: labgrid_coordinator_pb2.PollReservationRequest, context): token = request.token @@ -1156,13 +1248,14 @@ async def PollReservation(self, request: labgrid_coordinator_pb2.PollReservation res.refresh() return labgrid_coordinator_pb2.PollReservationResponse(reservation=res.as_pb2()) + @require_capability(Capability.get_reservations) @locked async def GetReservations(self, request: labgrid_coordinator_pb2.GetReservationsRequest, context): reservations = [x.as_pb2() for x in self.reservations.values()] return labgrid_coordinator_pb2.GetReservationsResponse(reservations=reservations) -async def serve(listen, cleanup) -> None: +async def serve(listen, cleanup, capabilities) -> None: asyncio.current_task().set_name("coordinator-serve") # It seems since https://github.com/grpc/grpc/pull/34647, the # ping_timeout_ms default of 60 seconds overrides keepalive_timeout_ms, @@ -1181,7 +1274,7 @@ async def serve(listen, cleanup) -> None: options=channel_options, interceptors=[IdentityServerInterceptor(client_identity_context)], ) - coordinator = Coordinator() + coordinator = Coordinator(capabilities) labgrid_coordinator_pb2_grpc.add_CoordinatorServicer_to_server(coordinator, server) # enable reflection for use with grpcurl reflection.enable_server_reflection( @@ -1244,6 +1337,7 @@ def main(): parser.add_argument( "--pystuck-port", metavar="PORT", type=int, default=6666, help="use a different pystuck port than 6666" ) + parser.add_argument("--capabilities", action="store_true", default=False, help="enable using capabilities, which also enforces client identities") args = parser.parse_args() @@ -1268,7 +1362,7 @@ def main(): cleanup = [] loop.set_debug(True) try: - loop.run_until_complete(serve(args.listen, cleanup)) + loop.run_until_complete(serve(args.listen, cleanup, args.capabilities)) finally: if cleanup: loop.run_until_complete(*cleanup) diff --git a/labgrid/remote/grpc/interceptor/server.py b/labgrid/remote/grpc/interceptor/server.py index 3727fced6..d50d7ded7 100644 --- a/labgrid/remote/grpc/interceptor/server.py +++ b/labgrid/remote/grpc/interceptor/server.py @@ -22,6 +22,7 @@ async def intercept_service(self, continuation, handler_call_details): metadata = handler_call_details.invocation_metadata logging.debug(metadata) + print(f"Server metadata: {metadata}") try: client_identity = ClientIdentity.from_metadata(metadata) From b7b8395b9493e728d4a1e9e0d3bbeac6b93fb660 Mon Sep 17 00:00:00 2001 From: Rouven Czerwinski Date: Wed, 12 Aug 2026 18:30:33 +0200 Subject: [PATCH 7/8] remote: client: return token for reservation To make ClientSession testing easier, return the token back to the user when calling create_reservation(). Signed-off-by: Rouven Czerwinski --- labgrid/remote/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 50150d50d..b9e720550 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -1596,6 +1596,8 @@ async def create_reservation(self): print("Waiting for allocation...") await self._wait_reservation(res.token, verbose=False) + return res.token + async def cancel_reservation(self): token: str = self.args.token From deff9a2dee4cd2e509edd264a98ebd9c28db5685 Mon Sep 17 00:00:00 2001 From: Rouven Czerwinski Date: Thu, 13 Aug 2026 17:51:33 +0200 Subject: [PATCH 8/8] fixup! coordinator: implement capabilities Apply comments from asher & run ruff. Signed-off-by: Rouven Czerwinski --- labgrid/remote/coordinator.py | 85 +++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index f44514949..c56e9cddb 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -191,11 +191,15 @@ async def wrapper(self, *args, **kwargs): return wrapper + async def check_capability(cls, identity, capability, context): if not identity and cls.use_capabilities == True: await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") if identity and (capability not in identity.capabilities): - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {req_cap} not in client capabilities {ctx.capabilities}") + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, f"Capability {req_cap} not in client capabilities {ctx.capabilities}" + ) + def require_capability(req_cap): def decorator(func): @@ -206,6 +210,7 @@ async def wrapper(self, request, context): return await func(self, request, context) return wrapper + return decorator @@ -985,7 +990,10 @@ async def ReleasePlace(self, request, context): fromuser = request.fromuser if request.HasField("fromuser") else None if fromuser and self.use_capabilities and not Capability.release_place_any in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}", + ) try: place = self.places[name] @@ -1001,18 +1009,30 @@ async def ReleasePlace(self, request, context): try: 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") - + await context.abort( + grpc.StatusCode.FAILED_PRECONDITION, f"Peer {context.peer()} does not have a valid session" + ) if self.use_capabilities: if not identity: - await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") + await context.abort( + grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities" + ) owned = username == place.acquired - if owned and not (Capability.release_place_owned in identity.capabilities or Capability.release_place_any in identity.capabilities): - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_owned} not in client capabilities {identity.capabilities}") + if owned and not ( + Capability.release_place_owned in identity.capabilities + or Capability.release_place_any in identity.capabilities + ): + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.release_place_owned} not in client capabilities {identity.capabilities}", + ) elif not owned and not Capability.release_place_any in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.release_place_any} not in client capabilities {identity.capabilities}", + ) await self._release_resources(place, place.acquired_resources) @@ -1051,11 +1071,22 @@ async def AllowPlace(self, request, context): if self.use_capabilities: if not identity: - await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") - if owned and not Capability.allow_place_owned in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.allow_place_owned} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities" + ) + if owned and not ( + Capability.allow_place_owned in identity.capabilities + or Capability.allow_place_any in identity.capabilities + ): + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.allow_place_owned} not in client capabilities {identity.capabilities}", + ) elif not owned and not Capability.allow_place_any in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.allow_place_any} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.allow_place_any} not in client capabilities {identity.capabilities}", + ) place.allowed.add(user) place.touch() @@ -1222,16 +1253,29 @@ async def CancelReservation(self, request: labgrid_coordinator_pb2.CancelReserva 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") + await context.abort( + grpc.StatusCode.FAILED_PRECONDITION, f"Peer {context.peer} does not have a valid session" + ) owned = self.reservations[token].owner == owner if self.use_capabilities: if not identity: - await context.abort(grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities") - if owned and not Capability.cancel_reservation_owned in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.UNAUTHENTICATED, "Client identity is required when using capabilities" + ) + if owned and not ( + Capability.cancel_reservation_owned in identity.capabilities + or Capability.cancel_reservation_any in identity.capabilities + ): + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}", + ) elif not owned and not Capability.cancel_reservation_any in identity.capabilities: - await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}") + await context.abort( + grpc.StatusCode.PERMISSION_DENIED, + f"Capability {Capability.cancel_reservation_any} not in client capabilities {identity.capabilities}", + ) del self.reservations[token] self.schedule_reservations() @@ -1337,7 +1381,12 @@ def main(): parser.add_argument( "--pystuck-port", metavar="PORT", type=int, default=6666, help="use a different pystuck port than 6666" ) - parser.add_argument("--capabilities", action="store_true", default=False, help="enable using capabilities, which also enforces client identities") + parser.add_argument( + "--capabilities", + action="store_true", + default=False, + help="enable using capabilities, which also enforces client identities", + ) args = parser.parse_args()