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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 <lab>-<device>
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
Expand Down
7 changes: 5 additions & 2 deletions ansible/files/coordinator/places.yaml.j2
Original file line number Diff line number Diff line change
@@ -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: []
Expand All @@ -10,9 +12,10 @@
matches:
- cls: '*'
exporter: '*'
group: {{ inventory_hostname }}-{{ device }}
group: {{ inventory_hostname }}-{{ instance }}
name: null
rename: null
tags:
device: {{ device }}
{% endfor %}
{% endfor %}
59 changes: 59 additions & 0 deletions docs/sharing-target-files.md
Original file line number Diff line number Diff line change
@@ -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/<lab>/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
62 changes: 59 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,80 @@

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):
config._metadata = getattr(config, "_metadata", {})
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)}'")
Expand Down
Loading