diff --git a/doc/man/client.rst b/doc/man/client.rst index 309e36365..ae7bcae61 100644 --- a/doc/man/client.rst +++ b/doc/man/client.rst @@ -49,6 +49,15 @@ LG_ENV This variable can be used to specify the configuration file to use without using the ``--config`` option, the ``--config`` option overrides it. +The special value ``coordinator:`` (or ``--config coordinator:``) tells the +client to fetch the environment from the coordinator over the ``GetEnvironment`` +RPC instead of reading a local file, which is useful when working from a remote +machine that does not have the lab env file available locally. The fetched +YAML is cached under ``$XDG_CACHE_HOME/labgrid/env.cfg`` (or +``~/.cache/labgrid/env.cfg``) so it can be inspected after the fact. The +coordinator must have been started with ``--environment``; see +``labgrid-coordinator``\(1). + LG_COORDINATOR ~~~~~~~~~~~~~~ This variable can be used to set the default coordinator in the format diff --git a/doc/man/coordinator.rst b/doc/man/coordinator.rst index c89eccad4..2dd9f755b 100644 --- a/doc/man/coordinator.rst +++ b/doc/man/coordinator.rst @@ -25,9 +25,23 @@ OPTIONS display command line help -l ADDRESS, --listen ADDRESS make coordinator listen on host and port +-e FILE, --environment FILE + serve the given YAML env file to clients via the ``GetEnvironment`` RPC. + Clients opt in by setting ``LG_ENV=coordinator:`` (see + ``labgrid-client``\(1)) -d, --debug enable debug mode +-e / --environment +~~~~~~~~~~~~~~~~~~ +When this option is set the coordinator reads the file fresh on each +``GetEnvironment`` request and returns its contents verbatim to the client. +This means a remote user no longer needs a local copy of the env file - they +only need network access to the coordinator. + +The default (no ``--environment``) is unchanged: ``GetEnvironment`` returns an +empty string and clients keep loading env from a local file as before. + SEE ALSO -------- diff --git a/doc/usage.rst b/doc/usage.rst index 37527fb40..ea29be724 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -166,6 +166,37 @@ allocated before returning. A reservation will time out after a short time, if it is neither refreshed nor used by locked places. +Fetching the Environment from the Coordinator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When several developers (or CI jobs) share a lab, distributing the env file out +of band - via a shared filesystem, git checkout or scp - is awkward duplication. +``labgrid-coordinator`` can serve a curated env to clients on demand, so a +remote user only needs network access to the coordinator and a labgrid install. + +Start the coordinator with ``--environment`` pointing at a YAML file: + +.. code-block:: bash + + $ labgrid-coordinator --environment /etc/labgrid/lab.cfg + +On the client side, set ``LG_ENV=coordinator:`` (or ``--config coordinator:``) +to fetch the env via the ``GetEnvironment`` RPC instead of reading a local file. +The returned YAML is cached under ``$XDG_CACHE_HOME/labgrid/env.cfg`` (or +``~/.cache/labgrid/env.cfg``), overwritten on each fetch so a user can inspect +what env the client just loaded. + +.. code-block:: bash + + $ export LG_COORDINATOR=lab-host:20408 + $ export LG_ENV=coordinator: + $ labgrid-client console -p + +Per-user paths inside the served env (build dirs, log dirs, source trees) are +still resolvable via the existing ``LG_*`` template substitution, so individual +clients can override only the few values that matter to them without forking the +env file. + Library ------- labgrid can be used directly as a Python library, without the infrastructure diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 4d2eb0bfa..115905f8d 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -1725,6 +1725,40 @@ def start_session( return session +def fetch_coordinator_environment(address): + """Fetch the env file served by the coordinator and cache it on disk + + Used when the user sets ``LG_ENV=coordinator:`` (or + ``--config coordinator:``) instead of pointing at a local file. + + Returns the path to the cached file (under ``$XDG_CACHE_HOME`` or + ``~/.cache/labgrid/env.cfg``), overwritten on each call so users + can inspect what env the client just loaded. Raises UserError if + the coordinator is not configured to serve an environment. + """ + address = proxymanager.get_grpc_address(address, default_port=20408) + with grpc.insecure_channel(address) as channel: + stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(channel) + try: + response = stub.GetEnvironment( + labgrid_coordinator_pb2.GetEnvironmentRequest(), + timeout=10.0, + ) + except grpc.RpcError as e: + # pylint: disable-next=no-member + raise UserError(f"failed to fetch environment from coordinator at {address}: {e.details() or e}") from e + if not response.config: + raise UserError( + f"coordinator at {address} has no environment configured (start coordinator with --environment )" + ) + cache_dir = os.path.join(os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")), "labgrid") + os.makedirs(cache_dir, exist_ok=True) + path = os.path.join(cache_dir, "env.cfg") + with open(path, "w", encoding="utf-8") as f: + f.write(response.config) + return path + + def find_role_by_place(config, place): for role, role_config in config.items(): resources, _ = target_factory.normalize_config(role_config) @@ -2305,6 +2339,17 @@ def main(): if args.proxy: proxymanager.force_proxy(args.proxy) + if args.config == "coordinator:": + # Fetch the env from the coordinator. The address can't be taken from + # env.config yet (no env loaded), so use the same fallback chain the + # rest of main() uses, minus that. + addr = args.coordinator or os.environ.get("LG_COORDINATOR", "127.0.0.1:20408") + try: + args.config = fetch_coordinator_environment(addr) + except UserError as e: + print(e, file=sys.stderr) + exit(1) + env = None if args.config: env = Environment(config_file=args.config) diff --git a/labgrid/remote/coordinator.py b/labgrid/remote/coordinator.py index a63a21482..87605734f 100644 --- a/labgrid/remote/coordinator.py +++ b/labgrid/remote/coordinator.py @@ -209,11 +209,12 @@ class ExporterError(Exception): class Coordinator(labgrid_coordinator_pb2_grpc.CoordinatorServicer): - def __init__(self) -> None: + def __init__(self, environment_file: str | None = None) -> None: self.places: dict[str, Place] = {} self.reservations = {} self.poll_tasks = [] self.save_scheduled = False + self.environment_file = environment_file self.lock = asyncio.Lock() self.exporters: dict[str, ExporterSession] = {} @@ -1105,8 +1106,17 @@ async def GetReservations(self, request: labgrid_coordinator_pb2.GetReservations reservations = [x.as_pb2() for x in self.reservations.values()] return labgrid_coordinator_pb2.GetReservationsResponse(reservations=reservations) + async def GetEnvironment(self, request: labgrid_coordinator_pb2.GetEnvironmentRequest, context): + if not self.environment_file: + return labgrid_coordinator_pb2.GetEnvironmentResponse(config="") + try: + with open(self.environment_file, encoding="utf-8") as f: + return labgrid_coordinator_pb2.GetEnvironmentResponse(config=f.read()) + except OSError as e: + await context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"cannot read environment file: {e}") + -async def serve(listen, cleanup) -> None: +async def serve(listen, cleanup, environment_file=None) -> 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, @@ -1124,7 +1134,7 @@ async def serve(listen, cleanup) -> None: server = grpc.aio.server( options=channel_options, ) - coordinator = Coordinator() + coordinator = Coordinator(environment_file=environment_file) labgrid_coordinator_pb2_grpc.add_CoordinatorServicer_to_server(coordinator, server) # enable reflection for use with grpcurl reflection.enable_server_reflection( @@ -1172,6 +1182,15 @@ def main(): default="[::]:20408", help="coordinator listening host and port", ) + parser.add_argument( + "-e", + "--environment", + metavar="FILE", + type=str, + default=None, + help="path to a YAML environment file to serve to clients via " + "GetEnvironment (LG_ENV=coordinator: on the client side)", + ) parser.add_argument("-d", "--debug", action="store_true", default=False, help="enable debug mode") parser.add_argument("--pystuck", action="store_true", help="enable pystuck") parser.add_argument( @@ -1201,7 +1220,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, environment_file=args.environment)) finally: if cleanup: loop.run_until_complete(*cleanup) diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2.py b/labgrid/remote/generated/labgrid_coordinator_pb2.py index 37652bff7..fff357073 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2.py +++ b/labgrid/remote/generated/labgrid_coordinator_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19labgrid-coordinator.proto\x12\x07labgrid\"\x8a\x01\n\x0f\x43lientInMessage\x12\x1d\n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12\'\n\tsubscribe\x18\x03 \x01(\x0b\x32\x12.labgrid.SubscribeH\x00\x42\x06\n\x04kind\"\x12\n\x04Sync\x12\n\n\x02id\x18\x01 \x01(\x04\",\n\x0bStartupDone\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\tSubscribe\x12\x1b\n\x0eis_unsubscribe\x18\x01 \x01(\x08H\x01\x88\x01\x01\x12\x14\n\nall_places\x18\x02 \x01(\x08H\x00\x12\x17\n\rall_resources\x18\x03 \x01(\x08H\x00\x42\x06\n\x04kindB\x11\n\x0f_is_unsubscribe\"g\n\x10\x43lientOutMessage\x12 \n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x88\x01\x01\x12(\n\x07updates\x18\x02 \x03(\x0b\x32\x17.labgrid.UpdateResponseB\x07\n\x05_sync\"\xa5\x01\n\x0eUpdateResponse\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12.\n\x0c\x64\x65l_resource\x18\x02 \x01(\x0b\x32\x16.labgrid.Resource.PathH\x00\x12\x1f\n\x05place\x18\x03 \x01(\x0b\x32\x0e.labgrid.PlaceH\x00\x12\x13\n\tdel_place\x18\x04 \x01(\tH\x00\x42\x06\n\x04kind\"\x9a\x01\n\x11\x45xporterInMessage\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12-\n\x08response\x18\x03 \x01(\x0b\x32\x19.labgrid.ExporterResponseH\x00\x42\x06\n\x04kind\"\x9e\x03\n\x08Resource\x12$\n\x04path\x18\x01 \x01(\x0b\x32\x16.labgrid.Resource.Path\x12\x0b\n\x03\x63ls\x18\x02 \x01(\t\x12-\n\x06params\x18\x03 \x03(\x0b\x32\x1d.labgrid.Resource.ParamsEntry\x12+\n\x05\x65xtra\x18\x04 \x03(\x0b\x32\x1c.labgrid.Resource.ExtraEntry\x12\x10\n\x08\x61\x63quired\x18\x05 \x01(\t\x12\r\n\x05\x61vail\x18\x06 \x01(\x08\x1a_\n\x04Path\x12\x1a\n\rexporter_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ngroup_name\x18\x02 \x01(\t\x12\x15\n\rresource_name\x18\x03 \x01(\tB\x10\n\x0e_exporter_name\x1a@\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\x1a?\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\"\x82\x01\n\x08MapValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nuint_value\x18\x03 \x01(\x04H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x05 \x01(\tH\x00\x42\x06\n\x04kind\"C\n\x10\x45xporterResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x06reason\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_reason\"\x18\n\x05Hello\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x82\x01\n\x12\x45xporterOutMessage\x12\x1f\n\x05hello\x18\x01 \x01(\x0b\x32\x0e.labgrid.HelloH\x00\x12\x43\n\x14set_acquired_request\x18\x02 \x01(\x0b\x32#.labgrid.ExporterSetAcquiredRequestH\x00\x42\x06\n\x04kind\"o\n\x1a\x45xporterSetAcquiredRequest\x12\x12\n\ngroup_name\x18\x01 \x01(\t\x12\x15\n\rresource_name\x18\x02 \x01(\t\x12\x17\n\nplace_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\r\n\x0b_place_name\"\x1f\n\x0f\x41\x64\x64PlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x12\n\x10\x41\x64\x64PlaceResponse\"\"\n\x12\x44\x65letePlaceRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x15\n\x13\x44\x65letePlaceResponse\"\x12\n\x10GetPlacesRequest\"3\n\x11GetPlacesResponse\x12\x1e\n\x06places\x18\x01 \x03(\x0b\x32\x0e.labgrid.Place\"\xd2\x02\n\x05Place\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07\x61liases\x18\x02 \x03(\t\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12&\n\x04tags\x18\x04 \x03(\x0b\x32\x18.labgrid.Place.TagsEntry\x12\'\n\x07matches\x18\x05 \x03(\x0b\x32\x16.labgrid.ResourceMatch\x12\x15\n\x08\x61\x63quired\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x1a\n\x12\x61\x63quired_resources\x18\x07 \x03(\t\x12\x0f\n\x07\x61llowed\x18\x08 \x03(\t\x12\x0f\n\x07\x63reated\x18\t \x01(\x01\x12\x0f\n\x07\x63hanged\x18\n \x01(\x01\x12\x18\n\x0breservation\x18\x0b \x01(\tH\x01\x88\x01\x01\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x0b\n\t_acquiredB\x0e\n\x0c_reservation\"y\n\rResourceMatch\x12\x10\n\x08\x65xporter\x18\x01 \x01(\t\x12\r\n\x05group\x18\x02 \x01(\t\x12\x0b\n\x03\x63ls\x18\x03 \x01(\t\x12\x11\n\x04name\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06rename\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\t\n\x07_rename\"8\n\x14\x41\x64\x64PlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x17\n\x15\x41\x64\x64PlaceAliasResponse\";\n\x17\x44\x65letePlaceAliasRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\x1a\n\x18\x44\x65letePlaceAliasResponse\"\x8b\x01\n\x13SetPlaceTagsRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x34\n\x04tags\x18\x02 \x03(\x0b\x32&.labgrid.SetPlaceTagsRequest.TagsEntry\x1a+\n\tTagsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x16\n\x14SetPlaceTagsResponse\"<\n\x16SetPlaceCommentRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07\x63omment\x18\x02 \x01(\t\"\x19\n\x17SetPlaceCommentResponse\"Z\n\x14\x41\x64\x64PlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x17\n\x15\x41\x64\x64PlaceMatchResponse\"]\n\x17\x44\x65letePlaceMatchRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0f\n\x07pattern\x18\x02 \x01(\t\x12\x13\n\x06rename\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_rename\"\x1a\n\x18\x44\x65letePlaceMatchResponse\"(\n\x13\x41\x63quirePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\"\x16\n\x14\x41\x63quirePlaceResponse\"L\n\x13ReleasePlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x15\n\x08\x66romuser\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0b\n\t_fromuser\"\x16\n\x14ReleasePlaceResponse\"4\n\x11\x41llowPlaceRequest\x12\x11\n\tplacename\x18\x01 \x01(\t\x12\x0c\n\x04user\x18\x02 \x01(\t\"\x14\n\x12\x41llowPlaceResponse\"\xb6\x01\n\x18\x43reateReservationRequest\x12?\n\x07\x66ilters\x18\x01 \x03(\x0b\x32..labgrid.CreateReservationRequest.FiltersEntry\x12\x0c\n\x04prio\x18\x02 \x01(\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\"F\n\x19\x43reateReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"\xcd\x03\n\x0bReservation\x12\r\n\x05owner\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\r\n\x05state\x18\x03 \x01(\x05\x12\x0c\n\x04prio\x18\x04 \x01(\x01\x12\x32\n\x07\x66ilters\x18\x05 \x03(\x0b\x32!.labgrid.Reservation.FiltersEntry\x12:\n\x0b\x61llocations\x18\x06 \x03(\x0b\x32%.labgrid.Reservation.AllocationsEntry\x12\x0f\n\x07\x63reated\x18\x07 \x01(\x01\x12\x0f\n\x07timeout\x18\x08 \x01(\x01\x1ap\n\x06\x46ilter\x12\x37\n\x06\x66ilter\x18\x01 \x03(\x0b\x32\'.labgrid.Reservation.Filter.FilterEntry\x1a-\n\x0b\x46ilterEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1aK\n\x0c\x46iltersEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12*\n\x05value\x18\x02 \x01(\x0b\x32\x1b.labgrid.Reservation.Filter:\x02\x38\x01\x1a\x32\n\x10\x41llocationsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\")\n\x18\x43\x61ncelReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"\x1b\n\x19\x43\x61ncelReservationResponse\"\'\n\x16PollReservationRequest\x12\r\n\x05token\x18\x01 \x01(\t\"D\n\x17PollReservationResponse\x12)\n\x0breservation\x18\x01 \x01(\x0b\x32\x14.labgrid.Reservation\"E\n\x17GetReservationsResponse\x12*\n\x0creservations\x18\x01 \x03(\x0b\x32\x14.labgrid.Reservation\"\x18\n\x16GetReservationsRequest2\xd2\x0b\n\x0b\x43oordinator\x12I\n\x0c\x43lientStream\x12\x18.labgrid.ClientInMessage\x1a\x19.labgrid.ClientOutMessage\"\x00(\x01\x30\x01\x12O\n\x0e\x45xporterStream\x12\x1a.labgrid.ExporterInMessage\x1a\x1b.labgrid.ExporterOutMessage\"\x00(\x01\x30\x01\x12\x41\n\x08\x41\x64\x64Place\x12\x18.labgrid.AddPlaceRequest\x1a\x19.labgrid.AddPlaceResponse\"\x00\x12J\n\x0b\x44\x65letePlace\x12\x1b.labgrid.DeletePlaceRequest\x1a\x1c.labgrid.DeletePlaceResponse\"\x00\x12\x44\n\tGetPlaces\x12\x19.labgrid.GetPlacesRequest\x1a\x1a.labgrid.GetPlacesResponse\"\x00\x12P\n\rAddPlaceAlias\x12\x1d.labgrid.AddPlaceAliasRequest\x1a\x1e.labgrid.AddPlaceAliasResponse\"\x00\x12Y\n\x10\x44\x65letePlaceAlias\x12 .labgrid.DeletePlaceAliasRequest\x1a!.labgrid.DeletePlaceAliasResponse\"\x00\x12M\n\x0cSetPlaceTags\x12\x1c.labgrid.SetPlaceTagsRequest\x1a\x1d.labgrid.SetPlaceTagsResponse\"\x00\x12V\n\x0fSetPlaceComment\x12\x1f.labgrid.SetPlaceCommentRequest\x1a .labgrid.SetPlaceCommentResponse\"\x00\x12P\n\rAddPlaceMatch\x12\x1d.labgrid.AddPlaceMatchRequest\x1a\x1e.labgrid.AddPlaceMatchResponse\"\x00\x12Y\n\x10\x44\x65letePlaceMatch\x12 .labgrid.DeletePlaceMatchRequest\x1a!.labgrid.DeletePlaceMatchResponse\"\x00\x12M\n\x0c\x41\x63quirePlace\x12\x1c.labgrid.AcquirePlaceRequest\x1a\x1d.labgrid.AcquirePlaceResponse\"\x00\x12M\n\x0cReleasePlace\x12\x1c.labgrid.ReleasePlaceRequest\x1a\x1d.labgrid.ReleasePlaceResponse\"\x00\x12G\n\nAllowPlace\x12\x1a.labgrid.AllowPlaceRequest\x1a\x1b.labgrid.AllowPlaceResponse\"\x00\x12\\\n\x11\x43reateReservation\x12!.labgrid.CreateReservationRequest\x1a\".labgrid.CreateReservationResponse\"\x00\x12\\\n\x11\x43\x61ncelReservation\x12!.labgrid.CancelReservationRequest\x1a\".labgrid.CancelReservationResponse\"\x00\x12V\n\x0fPollReservation\x12\x1f.labgrid.PollReservationRequest\x1a .labgrid.PollReservationResponse\"\x00\x12V\n\x0fGetReservations\x12\x1f.labgrid.GetReservationsRequest\x1a .labgrid.GetReservationsResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19labgrid-coordinator.proto\x12\x07labgrid\"\x8a\x01\n\x0f\x43lientInMessage\x12\x1d\n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12\'\n\tsubscribe\x18\x03 \x01(\x0b\x32\x12.labgrid.SubscribeH\x00\x42\x06\n\x04kind\"\x12\n\x04Sync\x12\n\n\x02id\x18\x01 \x01(\x04\",\n\x0bStartupDone\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"r\n\tSubscribe\x12\x1b\n\x0eis_unsubscribe\x18\x01 \x01(\x08H\x01\x88\x01\x01\x12\x14\n\nall_places\x18\x02 \x01(\x08H\x00\x12\x17\n\rall_resources\x18\x03 \x01(\x08H\x00\x42\x06\n\x04kindB\x11\n\x0f_is_unsubscribe\"g\n\x10\x43lientOutMessage\x12 \n\x04sync\x18\x01 \x01(\x0b\x32\r.labgrid.SyncH\x00\x88\x01\x01\x12(\n\x07updates\x18\x02 \x03(\x0b\x32\x17.labgrid.UpdateResponseB\x07\n\x05_sync\"\xa5\x01\n\x0eUpdateResponse\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12.\n\x0c\x64\x65l_resource\x18\x02 \x01(\x0b\x32\x16.labgrid.Resource.PathH\x00\x12\x1f\n\x05place\x18\x03 \x01(\x0b\x32\x0e.labgrid.PlaceH\x00\x12\x13\n\tdel_place\x18\x04 \x01(\tH\x00\x42\x06\n\x04kind\"\x9a\x01\n\x11\x45xporterInMessage\x12%\n\x08resource\x18\x01 \x01(\x0b\x32\x11.labgrid.ResourceH\x00\x12\'\n\x07startup\x18\x02 \x01(\x0b\x32\x14.labgrid.StartupDoneH\x00\x12-\n\x08response\x18\x03 \x01(\x0b\x32\x19.labgrid.ExporterResponseH\x00\x42\x06\n\x04kind\"\x9e\x03\n\x08Resource\x12$\n\x04path\x18\x01 \x01(\x0b\x32\x16.labgrid.Resource.Path\x12\x0b\n\x03\x63ls\x18\x02 \x01(\t\x12-\n\x06params\x18\x03 \x03(\x0b\x32\x1d.labgrid.Resource.ParamsEntry\x12+\n\x05\x65xtra\x18\x04 \x03(\x0b\x32\x1c.labgrid.Resource.ExtraEntry\x12\x10\n\x08\x61\x63quired\x18\x05 \x01(\t\x12\r\n\x05\x61vail\x18\x06 \x01(\x08\x1a_\n\x04Path\x12\x1a\n\rexporter_name\x18\x01 \x01(\tH\x00\x88\x01\x01\x12\x12\n\ngroup_name\x18\x02 \x01(\t\x12\x15\n\rresource_name\x18\x03 \x01(\tB\x10\n\x0e_exporter_name\x1a@\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\x1a?\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.labgrid.MapValue:\x02\x38\x01\"\x82\x01\n\x08MapValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x02 \x01(\x03H\x00\x12\x14\n\nuint_value\x18\x03 \x01(\x04H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12\x16\n\x0cstring_value\x18\x05 \x01(\tH\x00\x42\x06\n\x04kind\"C\n\x10\x45xporterResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x06reason\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\t\n\x07_reason\"\x18\n\x05Hello\x12\x0f\n\x07version\x18\x01 \x01(\t\"\x82\x01\n\x12\x45xporterOutMessage\x12\x1f\n\x05hello\x18\x01 \x01(\x0b\x32\x0e.labgrid.HelloH\x00\x12\x43\n\x14set_acquired_request\x18\x02 \x01(\x0b\x32#.labgrid.ExporterSetAcquiredRequestH\x00\x42\x06\n\x04kind\"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\x16GetReservationsRequest\"\x17\n\x15GetEnvironmentRequest\"(\n\x16GetEnvironmentResponse\x12\x0e\n\x06\x63onfig\x18\x01 \x01(\t2\xa7\x0c\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\x12S\n\x0eGetEnvironment\x12\x1e.labgrid.GetEnvironmentRequest\x1a\x1f.labgrid.GetEnvironmentResponse\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -153,6 +153,10 @@ _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['_GETENVIRONMENTREQUEST']._serialized_start=4243 + _globals['_GETENVIRONMENTREQUEST']._serialized_end=4266 + _globals['_GETENVIRONMENTRESPONSE']._serialized_start=4268 + _globals['_GETENVIRONMENTRESPONSE']._serialized_end=4308 + _globals['_COORDINATOR']._serialized_start=4311 + _globals['_COORDINATOR']._serialized_end=5886 # @@protoc_insertion_point(module_scope) diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2.pyi b/labgrid/remote/generated/labgrid_coordinator_pb2.pyi index 366f4e438..5768d022a 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2.pyi +++ b/labgrid/remote/generated/labgrid_coordinator_pb2.pyi @@ -446,3 +446,13 @@ class GetReservationsResponse(_message.Message): class GetReservationsRequest(_message.Message): __slots__ = () def __init__(self) -> None: ... + +class GetEnvironmentRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class GetEnvironmentResponse(_message.Message): + __slots__ = ("config",) + CONFIG_FIELD_NUMBER: _ClassVar[int] + config: str + def __init__(self, config: _Optional[str] = ...) -> None: ... diff --git a/labgrid/remote/generated/labgrid_coordinator_pb2_grpc.py b/labgrid/remote/generated/labgrid_coordinator_pb2_grpc.py index debfb24f2..be73f3969 100644 --- a/labgrid/remote/generated/labgrid_coordinator_pb2_grpc.py +++ b/labgrid/remote/generated/labgrid_coordinator_pb2_grpc.py @@ -104,6 +104,11 @@ def __init__(self, channel): request_serializer=labgrid__coordinator__pb2.GetReservationsRequest.SerializeToString, response_deserializer=labgrid__coordinator__pb2.GetReservationsResponse.FromString, ) + self.GetEnvironment = channel.unary_unary( + '/labgrid.Coordinator/GetEnvironment', + request_serializer=labgrid__coordinator__pb2.GetEnvironmentRequest.SerializeToString, + response_deserializer=labgrid__coordinator__pb2.GetEnvironmentResponse.FromString, + ) class CoordinatorServicer(object): @@ -217,6 +222,12 @@ def GetReservations(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetEnvironment(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_CoordinatorServicer_to_server(servicer, server): rpc_method_handlers = { @@ -310,6 +321,11 @@ def add_CoordinatorServicer_to_server(servicer, server): request_deserializer=labgrid__coordinator__pb2.GetReservationsRequest.FromString, response_serializer=labgrid__coordinator__pb2.GetReservationsResponse.SerializeToString, ), + 'GetEnvironment': grpc.unary_unary_rpc_method_handler( + servicer.GetEnvironment, + request_deserializer=labgrid__coordinator__pb2.GetEnvironmentRequest.FromString, + response_serializer=labgrid__coordinator__pb2.GetEnvironmentResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'labgrid.Coordinator', rpc_method_handlers) @@ -625,3 +641,20 @@ def GetReservations(request, labgrid__coordinator__pb2.GetReservationsResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetEnvironment(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/labgrid.Coordinator/GetEnvironment', + labgrid__coordinator__pb2.GetEnvironmentRequest.SerializeToString, + labgrid__coordinator__pb2.GetEnvironmentResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/labgrid/remote/proto/labgrid-coordinator.proto b/labgrid/remote/proto/labgrid-coordinator.proto index e0585f7e1..471f174be 100644 --- a/labgrid/remote/proto/labgrid-coordinator.proto +++ b/labgrid/remote/proto/labgrid-coordinator.proto @@ -38,6 +38,8 @@ service Coordinator { rpc PollReservation(PollReservationRequest) returns (PollReservationResponse) {} rpc GetReservations(GetReservationsRequest) returns (GetReservationsResponse) {} + + rpc GetEnvironment(GetEnvironmentRequest) returns (GetEnvironmentResponse) {} } message ClientInMessage { @@ -295,3 +297,13 @@ message GetReservationsResponse { message GetReservationsRequest { }; + +message GetEnvironmentRequest { +}; + +message GetEnvironmentResponse { + // Raw text of the lab environment file (typically YAML) curated by + // the lab admin and served to clients. Empty if the coordinator was + // started without a configured environment file. + string config = 1; +}; diff --git a/man/labgrid-client.1 b/man/labgrid-client.1 index dfc95b2e8..4aeea263a 100644 --- a/man/labgrid-client.1 +++ b/man/labgrid-client.1 @@ -1145,6 +1145,15 @@ A desired state must be set using \fBLG_STATE\fP or \fB\-s\fP/\fB\-\-state\fP\&. .sp This variable can be used to specify the configuration file to use without using the \fB\-\-config\fP option, the \fB\-\-config\fP option overrides it. +.sp +The special value \fBcoordinator:\fP (or \fB\-\-config coordinator:\fP) tells the +client to fetch the environment from the coordinator over the \fBGetEnvironment\fP +RPC instead of reading a local file, which is useful when working from a remote +machine that does not have the lab env file available locally. The fetched +YAML is cached under \fB$XDG_CACHE_HOME/labgrid/env.cfg\fP (or +\fB~/.cache/labgrid/env.cfg\fP) so it can be inspected after the fact. The +coordinator must have been started with \fB\-\-environment\fP; see +\fBlabgrid\-coordinator\fP(1). .SS LG_COORDINATOR .sp This variable can be used to set the default coordinator in the format diff --git a/man/labgrid-coordinator.1 b/man/labgrid-coordinator.1 index e99c30f93..ddb620ef5 100644 --- a/man/labgrid-coordinator.1 +++ b/man/labgrid-coordinator.1 @@ -51,9 +51,23 @@ display command line help .BI \-l \ ADDRESS\fR,\fB \ \-\-listen \ ADDRESS make coordinator listen on host and port .TP +.BI \-e \ FILE\fR,\fB \ \-\-environment \ FILE +serve the given YAML env file to clients via the \fBGetEnvironment\fP RPC. +Clients opt in by setting \fBLG_ENV=coordinator:\fP (see +\fBlabgrid\-client\fP(1)) +.TP .B \-d\fP,\fB \-\-debug enable debug mode .UNINDENT +.SS \-e / \-\-environment +.sp +When this option is set the coordinator reads the file fresh on each +\fBGetEnvironment\fP request and returns its contents verbatim to the client. +This means a remote user no longer needs a local copy of the env file \- they +only need network access to the coordinator. +.sp +The default (no \fB\-\-environment\fP) is unchanged: \fBGetEnvironment\fP returns an +empty string and clients keep loading env from a local file as before. .SS SEE ALSO .sp \fBlabgrid\-client\fP(1), \fBlabgrid\-exporter\fP(1) diff --git a/tests/conftest.py b/tests/conftest.py index 3bb2641f9..3a8114ab5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -142,12 +142,15 @@ def start(self): class Coordinator(LabgridComponent): - def start(self): + def start(self, args=''): assert self.spawn is None assert self.reader is None + cmd = 'python -m labgrid.remote.coordinator' + if args: + cmd = f'{cmd} {args}' self.spawn = pexpect.spawn( - 'python -m labgrid.remote.coordinator', + cmd, logfile=Prefixer(sys.stdout.buffer, 'coordinator'), cwd=self.cwd) try: @@ -204,6 +207,17 @@ def coordinator(tmpdir): coordinator.stop() +@pytest.fixture(scope='function') +def coordinator_with_env(tmpdir): + env_file = tmpdir.join('env.yaml') + env_file.write('targets:\n') + coordinator = Coordinator(tmpdir) + coordinator.start(f'--environment {env_file}') + + yield coordinator, env_file + + coordinator.stop() + @pytest.fixture(scope='function') def exporter(tmpdir, coordinator): config = "exports.yaml" diff --git a/tests/test_client.py b/tests/test_client.py index 8d2cc1c9f..23f939c8c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -616,3 +616,35 @@ def test_same_name_resources(place, exporter, tmpdir): spawn.expect(pexpect.EOF) spawn.close() assert spawn.exitstatus == 0, spawn.before.strip() + + +def test_config_coordinator(coordinator_with_env, monkeypatch, tmpdir): + _, env_file = coordinator_with_env + env_content = ( + "targets:\n" + " main:\n" + " resources:\n" + " RemotePlace:\n" + " name: test\n" + ) + env_file.write(env_content) + + cache = tmpdir.join('cache') + monkeypatch.setenv("XDG_CACHE_HOME", str(cache)) + + with pexpect.spawn('python -m labgrid.remote.client -c coordinator: places') as spawn: + spawn.expect(pexpect.EOF) + assert spawn.exitstatus == 0, spawn.before.strip() + + cached = cache.join('labgrid', 'env.cfg') + assert cached.check(file=1), f"cache file missing: {cached}" + assert cached.read() == env_content + + +def test_config_coordinator_unconfigured(coordinator, monkeypatch, tmpdir): + monkeypatch.setenv("XDG_CACHE_HOME", str(tmpdir.join('cache'))) + + with pexpect.spawn('python -m labgrid.remote.client -c coordinator: places') as spawn: + spawn.expect("no environment configured") + spawn.expect(pexpect.EOF) + assert spawn.exitstatus == 1, spawn.before.strip() diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py index 2907baed0..726a9f098 100644 --- a/tests/test_coordinator.py +++ b/tests/test_coordinator.py @@ -4,6 +4,8 @@ import labgrid.remote.generated.labgrid_coordinator_pb2_grpc as labgrid_coordinator_pb2_grpc import labgrid.remote.generated.labgrid_coordinator_pb2 as labgrid_coordinator_pb2 +from conftest import Coordinator + @pytest.fixture(scope="function") def channel_stub(): @@ -191,3 +193,40 @@ def test_coordinator_create_reservation(coordinator, coordinator_place): assert res res: labgrid_coordinator_pb2.CreateReservationResponse assert len(res.reservation.token) > 0 + + +def test_coordinator_get_environment_default(coordinator, channel_stub): + res = channel_stub.GetEnvironment(labgrid_coordinator_pb2.GetEnvironmentRequest()) + assert res.config == "" + + +def test_coordinator_get_environment_serves_file(coordinator_with_env, channel_stub): + _, env_file = coordinator_with_env + env_file.write("targets:\n main: {}\n") + res = channel_stub.GetEnvironment(labgrid_coordinator_pb2.GetEnvironmentRequest()) + assert res.config == "targets:\n main: {}\n" + + +def test_coordinator_get_environment_refreshes(coordinator_with_env, channel_stub): + _, env_file = coordinator_with_env + env_file.write("targets:\n first: {}\n") + res = channel_stub.GetEnvironment(labgrid_coordinator_pb2.GetEnvironmentRequest()) + assert res.config == "targets:\n first: {}\n" + + env_file.write("targets:\n second: {}\n") + res = channel_stub.GetEnvironment(labgrid_coordinator_pb2.GetEnvironmentRequest()) + assert res.config == "targets:\n second: {}\n" + + +def test_coordinator_get_environment_missing_file(tmpdir): + coordinator = Coordinator(tmpdir) + missing = tmpdir.join("nope.yaml") + coordinator.start(f"--environment {missing}") + try: + with grpc.insecure_channel("127.0.0.1:20408") as channel: + stub = labgrid_coordinator_pb2_grpc.CoordinatorStub(channel) + with pytest.raises(grpc.RpcError) as excinfo: + stub.GetEnvironment(labgrid_coordinator_pb2.GetEnvironmentRequest()) + assert excinfo.value.code() == grpc.StatusCode.FAILED_PRECONDITION + finally: + coordinator.stop()