diff --git a/README.md b/README.md index 9784459e6..f5e16d1fc 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,9 @@ global-coordinator -> labgrid-aparcar -> openwrt-one ``` You can request access to existing labs or contribute your own. To do this, -submit a pull request modifying the `labnet.yaml` file. +submit a pull request modifying the `labnet.yaml` file. If you have multiple +devices of the same model, see [docs/sharing-target-files.md](docs/sharing-target-files.md) +for how to avoid duplicate target files. To access a remote device, configure the following environment variables. Notably, `LG_PROXY` sets the proxy host (always the lab name): @@ -111,9 +113,11 @@ Notably, `LG_PROXY` sets the proxy host (always the lab name): export LG_IMAGE=~/firmware/openwrt-ath79-generic-tplink_tl-wdr3600-v1-initramfs-kernel.bin # Firmware to boot export LG_PLACE=aparcar-tplink_tl-wdr3600-v1 # Target device, formatted as - export LG_PROXY=labgrid-aparcar # Proxy to use, typically the lab name -export LG_ENV=targets/tplink_tl-wdr3600-v1.yaml # Environment definition +export LG_ENV=targets/tplink_tl-wdr3600-v1.yaml # Environment definition (optional when using device_instances) ``` +**Note**: `LG_ENV` is optional when using `device_instances`. If not set, pytest automatically resolves the target file from `LG_PLACE`. Explicitly setting `LG_ENV` takes precedence over automatic resolution. See [docs/sharing-target-files.md](docs/sharing-target-files.md) for details. + To avoid interference from CI or other developers, lock the device before use: ```shell diff --git a/ansible/files/coordinator/places.yaml.j2 b/ansible/files/coordinator/places.yaml.j2 index c05f023d2..8cb2fd417 100644 --- a/ansible/files/coordinator/places.yaml.j2 +++ b/ansible/files/coordinator/places.yaml.j2 @@ -1,5 +1,7 @@ {% for device in labnet["labs"][inventory_hostname]["devices"] %} -{{ inventory_hostname }}-{{ device }}: +{% set device_instances = labnet["labs"][inventory_hostname].get("device_instances", {}).get(device, [device]) %} +{% for instance in device_instances %} +{{ inventory_hostname }}-{{ instance }}: acquired: null acquired_resources: [] aliases: [] @@ -10,9 +12,10 @@ matches: - cls: '*' exporter: '*' - group: {{ inventory_hostname }}-{{ device }} + group: {{ inventory_hostname }}-{{ instance }} name: null rename: null tags: device: {{ device }} {% endfor %} +{% endfor %} diff --git a/docs/sharing-target-files.md b/docs/sharing-target-files.md new file mode 100644 index 000000000..0b0fcff41 --- /dev/null +++ b/docs/sharing-target-files.md @@ -0,0 +1,59 @@ +# Sharing Target Files Across Multiple Devices + +When a lab has multiple physical devices of the same model (e.g., three Belkin RT3200 routers), creating separate target configuration files for each device leads to unnecessary duplication and maintenance overhead. + +If this is your situation, you can use the `device_instances` field in `labnet.yaml` to define multiple physical instances sharing a single target file: + +```yaml +devices: + linksys_e8450: + name: Linksys E8450 / Belkin RT3200 + target: mediatek-mt7622 + firmware: initramfs-kernel.bin + +labs: + labgrid-example: + proxy: labgrid-example + maintainers: "@maintainer" + devices: + - linksys_e8450 + device_instances: + linksys_e8450: + - router_1 + - router_2 + - router_3 +``` + +This will create three different labgrid places from the same configuration file. +- `labgrid-example-router_1` → `targets/linksys_e8450.yaml` +- `labgrid-example-router_2` → `targets/linksys_e8450.yaml` +- `labgrid-example-router_3` → `targets/linksys_e8450.yaml` + +## Configuration Separation + +- **Generic configuration** (drivers, prompts, strategies) → `targets/linksys_e8450.yaml` (shared) +- **Device-specific details** (serial ports, IPs, power) → `ansible/files/exporter//exporter.yaml` (per device) + +## Automatic Target Resolution + +When running tests with pytest, simply set `LG_PLACE` - the target file is resolved automatically: + +```bash +export LG_PLACE=labgrid-example-router_1 +export LG_PROXY=labgrid-example + +# LG_ENV is automatically set to targets/linksys_e8450.yaml +pytest tests/ --lg-log +``` + +## When to Use + +✅ Use `device_instances` when: +- You have multiple devices of the same model +- Devices require identical driver configurations +- Only device-specific details (serial, IP, power) differ + +❌ Don't use when: +- Devices need different driver configurations +- Firmware boot process differs between instances +- You only have one device of that specific model diff --git a/tests/conftest.py b/tests/conftest.py index a3f06b344..b43d157ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,17 +14,62 @@ import json import logging +import os from os import getenv +from pathlib import Path import pytest logger = logging.getLogger(__name__) -device = getenv("LG_ENV", "Unknown").split("/")[-1].split(".")[0] +def _resolve_target_from_place(): + lg_env = getenv("LG_ENV") + lg_place = getenv("LG_PLACE") -def pytest_addoption(parser): - parser.addoption("--firmware", action="store", default="firmware.bin") + if lg_env or not lg_place: + return lg_env + + parts = lg_place.split("-", 2) + if len(parts) < 3: + return None + + device_instance = parts[2] + + try: + repo_root = Path(__file__).parent.parent + labnet_path = repo_root / "labnet.yaml" + + if not labnet_path.exists(): + return None + + import yaml + + with open(labnet_path, "r") as f: + labnet = yaml.safe_load(f) + + if device_instance in labnet.get("devices", {}): + device_config = labnet["devices"][device_instance] + target_name = device_config.get("target_file", device_instance) + target_file = f"targets/{target_name}.yaml" + if (repo_root / target_file).exists(): + return str(repo_root / target_file) + + for lab_name, lab_config in labnet.get("labs", {}).items(): + device_instances = lab_config.get("device_instances", {}) + for base_device, instances in device_instances.items(): + if device_instance in instances: + if base_device in labnet.get("devices", {}): + device_config = labnet["devices"][base_device] + target_name = device_config.get("target_file", base_device) + target_file = f"targets/{target_name}.yaml" + if (repo_root / target_file).exists(): + return str(repo_root / target_file) + + except Exception: + pass + + return None def pytest_configure(config): @@ -32,6 +77,17 @@ def pytest_configure(config): config._metadata["version"] = "12.3.4" config._metadata["environment"] = "staging" + resolved_env = _resolve_target_from_place() + if resolved_env: + os.environ["LG_ENV"] = resolved_env + + +device = getenv("LG_ENV", "Unknown").split("/")[-1].split(".")[0] + + +def pytest_addoption(parser): + parser.addoption("--firmware", action="store", default="firmware.bin") + def ubus_call(command, namespace, method, params={}): output = command.run_check(f"ubus call {namespace} {method} '{json.dumps(params)}'")