Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/man/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions doc/man/coordinator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down
31 changes: 31 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <board>

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
Expand Down
45 changes: 45 additions & 0 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file>)"
)
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)
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 23 additions & 4 deletions labgrid/remote/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 7 additions & 3 deletions labgrid/remote/generated/labgrid_coordinator_pb2.py

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions labgrid/remote/generated/labgrid_coordinator_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
33 changes: 33 additions & 0 deletions labgrid/remote/generated/labgrid_coordinator_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
12 changes: 12 additions & 0 deletions labgrid/remote/proto/labgrid-coordinator.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ service Coordinator {
rpc PollReservation(PollReservationRequest) returns (PollReservationResponse) {}

rpc GetReservations(GetReservationsRequest) returns (GetReservationsResponse) {}

rpc GetEnvironment(GetEnvironmentRequest) returns (GetEnvironmentResponse) {}
}

message ClientInMessage {
Expand Down Expand Up @@ -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;
};
9 changes: 9 additions & 0 deletions man/labgrid-client.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions man/labgrid-coordinator.1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 16 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading