Skip to content
Draft
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
22 changes: 17 additions & 5 deletions labgrid/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .exceptions import InvalidConfigError, RegistrationError
from .util.dict import filter_dict
from .binding import BindingError


class TargetFactory:
Expand Down Expand Up @@ -145,19 +146,30 @@ def make_target(self, name, config, *, env=None):
from .target import Target

target = Target(name, env=env)
for item in TargetFactory._convert_to_named_list(config.get('resources', {})):
self.make_resources_from_config(target, config.get('resources', {}))
self.make_drivers_from_config(target, config.get('drivers', {}))
return target

def make_resources_from_config(self, target, resource_config):
for item in TargetFactory._convert_to_named_list(resource_config):
resource = item.pop('cls')
name = item.pop('name', None)
args = item # remaining args
self.make_resource(target, resource, name, args)
for item in TargetFactory._convert_to_named_list(config.get('drivers', {})):

def make_drivers_from_config(self, target, driver_config):
for item in TargetFactory._convert_to_named_list(driver_config):
driver = item.pop('cls')
name = item.pop('name', None)
bindings = item.pop('bindings', {})
args = item # remaining args
target.set_binding_map(bindings)
self.make_driver(target, driver, name, args)
return target
if target is not None:
target.set_binding_map(bindings)
try:
self.make_driver(target, driver, name, args)
except BindingError:
if target is not None:
raise

def class_from_string(self, string: str):
try:
Expand Down
97 changes: 95 additions & 2 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import sys
import shlex
import shutil
import tempfile
import json
import itertools
import ipaddress
import warnings
from textwrap import indent
from socket import gethostname
from getpass import getuser
Expand Down Expand Up @@ -46,7 +48,7 @@
from ..exceptions import NoDriverFoundError, NoResourceFoundError, InvalidConfigError
from .generated import labgrid_coordinator_pb2, labgrid_coordinator_pb2_grpc
from ..resource.remote import RemotePlaceManager, RemotePlace
from ..util import diff_dict, flat_dict, dump, atomic_replace, labgrid_version, Timeout
from ..util import diff_dict, flat_dict, dump, atomic_replace, labgrid_version, Timeout, load
from ..util.proxy import proxymanager
from ..util.helper import processwrapper
from ..driver import Mode, ExecutionError
Expand Down Expand Up @@ -853,14 +855,22 @@ def get_target_resources(self, place):
return resources

def get_target_config(self, place):
remote_env = place.get_remote_env()
config = {}
resources = config["resources"] = []
# resources from remote env
resources = config["resources"] = remote_env.get("resources", [])

# resources by match
for (name, _), resource in self.get_target_resources(place).items():
args = OrderedDict()
if name != resource.cls:
args["name"] = name
args.update(resource.args)
resources.append({resource.cls: args})

# drivers from remote env
config["drivers"] = remote_env.get("drivers", [])

return config

def print_env(self):
Expand Down Expand Up @@ -1660,6 +1670,86 @@ async def export(self, place, target):
def print_version(self):
print(labgrid_version())

async def edit_remote_env(self):
place = self.get_idle_place()
editor = os.environ.get("EDITOR", "vi")
remote_env = place.remote_env or ""
# remember last place change to be sent with SetPlaceRemoteEnvRequest for optimistic locking
changed = place.changed

# write current remote env to temporary file
with tempfile.NamedTemporaryFile(mode="w+", suffix=".tmp", delete=False, encoding="utf-8") as f:
path = f.name
f.write(remote_env)
f.flush()

try:
while True:
# open editor
subprocess.run(shlex.split(editor) + [path], check=True)

# process new config
with open(path, "r", encoding="utf-8") as f:
new_remote_env = f.read()

# sanity check new config
try:
with warnings.catch_warnings():
# turn UserWarnings emitted by labgrid.util's dict/yaml into exceptions to catch them
warnings.simplefilter("error", UserWarning)
# parse config
remote_conf = load(new_remote_env) or {}
# try to instantiate resources and drivers without a target and bindings
# (place is idle, resources cannot be bound, config may be incomplete)
target_factory.make_resources_from_config(None, remote_conf.get("resources", {}))
target_factory.make_drivers_from_config(None, remote_conf.get("drivers", {}))
except Exception as e:
# let user decide on errors during sanity checks
if self.args.debug:
traceback.print_exc(file=sys.stderr)
else:
print(f"Error: {e}")
key = None
while key is None or key.lower() not in ("", "y", "n", "q"):
key = input("Save anyway? [y]es, [N]o and reopen editor, [q]uit without saving ")

if key.lower() == "y":
# continue saving
pass
elif key.lower() == "q":
return
else:
# start over again
continue

# save remote env
try:
request = labgrid_coordinator_pb2.SetPlaceRemoteEnvRequest(
placename=place.name, changed=changed, remote_env=new_remote_env
)
await self.stub.SetPlaceRemoteEnv(request)
await self.sync_with_coordinator()
return
except grpc.aio.AioRpcError as e:
if self.args.debug:
traceback.print_exc(file=sys.stderr)
else:
print(f"Error: {e.details()}")
while key.lower() not in ("", "y", "n"):
key = input("Reopen editor? [Y]es, n[o] ")

if key.lower() == "n":
return
else:
# start over again
continue
finally:
# clean up temporary file
try:
os.remove(path)
except FileNotFoundError:
pass


_loop: ContextVar["asyncio.AbstractEventLoop | None"] = ContextVar("_loop", default=None)

Expand Down Expand Up @@ -2237,6 +2327,9 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
subparser = subparsers.add_parser("version", help="show version")
subparser.set_defaults(func=ClientSession.print_version)

subparser = subparsers.add_parser("edit", help="edit place's remote environment")
subparser.set_defaults(func=ClientSession.edit_remote_env)

return parser


Expand Down
16 changes: 16 additions & 0 deletions labgrid/remote/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import re
import string
import logging
import textwrap
from datetime import datetime
from fnmatch import fnmatchcase

import attr

from .generated import labgrid_coordinator_pb2
from ..util.yaml import load

__all__ = [
"TAG_KEY",
Expand Down Expand Up @@ -230,6 +232,7 @@ class Place:
created = attr.ib(default=attr.Factory(time.time))
changed = attr.ib(default=attr.Factory(time.time))
reservation = attr.ib(default=None)
remote_env = attr.ib(default=None)

def asdict(self):
# in the coordinator, we have resource objects, otherwise just a path
Expand All @@ -251,6 +254,7 @@ def asdict(self):
"created": self.created,
"changed": self.changed,
"reservation": self.reservation,
"remote_env": self.remote_env,
}

def update_from_pb2(self, place_pb2):
Expand Down Expand Up @@ -298,6 +302,15 @@ def show(self, level=0):
print(indent + f"changed: {datetime.fromtimestamp(self.changed)}")
if self.reservation:
print(indent + f"reservation: {self.reservation}")
if self.remote_env:
print(indent + "remote env:")
print(textwrap.indent(self.remote_env, indent + " "))

def get_remote_env(self):
if self.remote_env:
return load(self.remote_env)

return {}

def getmatch(self, resource_path):
"""Return the ResourceMatch object for the given resource path or None if not found.
Expand Down Expand Up @@ -350,6 +363,8 @@ def as_pb2(self):
place.created = self.created
if self.reservation:
place.reservation = self.reservation
if self.remote_env:
place.remote_env = self.remote_env
for key, value in self.tags.items():
place.tags[key] = value
return place
Expand Down Expand Up @@ -377,6 +392,7 @@ def from_pb2(cls, pb2):
created=pb2.created,
changed=pb2.changed,
reservation=pb2.reservation if pb2.HasField("reservation") else None,
remote_env=pb2.remote_env if pb2.HasField("remote_env") else None,
)


Expand Down
21 changes: 21 additions & 0 deletions labgrid/remote/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,27 @@ async def SetPlaceComment(self, request, context):
self.save_later()
return labgrid_coordinator_pb2.SetPlaceCommentResponse()

@locked
async def SetPlaceRemoteEnv(self, request, context):
placename = request.placename
changed = request.changed
remote_env = request.remote_env
try:
place = self.places[placename]
except KeyError:
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Place {placename} does not exist")

if place.changed != changed:
await context.abort(
grpc.StatusCode.FAILED_PRECONDITION, "Config was changed elsewhere during during your edit"
)

place.remote_env = remote_env
place.touch()
self._publish_place(place)
self.save_later()
return labgrid_coordinator_pb2.SetPlaceRemoteEnvResponse()

@locked
async def AddPlaceMatch(self, request, context):
placename = request.placename
Expand Down
Loading
Loading