The r2lab API

Testbed preparation

This module contains a utility for preparing the testbed.

The logic is that you write a scheduler that implements your pure experimental logic.

You can then pass this scheduler to r2lab_prepare_scheduler(), that returns a scheduler where your original one is embedded.

This overall scheduler will prepend instructions for preparing the testbed, in terms of loading images on nodes, turning off unused nodes, and similar.

r2lab.prepare.prepare_testbed_scheduler(gateway, load_flag, experiment_scheduler, images_mapping, nodes_left_alone=None, sdrs_left_alone=None, phones_left_alone=None, verbose_jobs=False)[source]

This function is designed as a standard way for experiments to warm up. Experimenters only need to write a scheduler that defines the behaviour of their core experiment, this function will add additional steps that take care of a) checking for a valid lease, b) load images on nodes, and c) turn off unused devices.

It is generally desirable to write an experiment script that has a –load/-l boolean flag; typically, one would use the --load flag the first time that an experiment is launched during a given timeslot, while subsequent calls won’t. That is the purpose of the load_flag below; when set to False, only step a) is performed, otherwise the resulting scheduler will go for the full monty.

Parameters:
  • gateway_sshnode – the ssh handle to the gateway

  • load_flag (bool) – if not set, only the lease is checked

  • experiment_scheduler (Scheduler) – core scheduler for the experiment

  • images_mapping – a dictionary that specifies images to be loaded on nodes; see examples below

  • nodes_left_alone – a list of node numbers that should be left intact, neither loaded nor turned off;

  • phones_left_alone – a list of node numbers that should be left intact, i.e. not switched to airplane mode.

Return :

The overall scheduler where the input experiment_scheduler is embedded.

Examples

Specify a mapping like the following:

images_mapping = { "ubuntu" : [1, 4, 5], "gnuradio": [16]}

Note that the format for images_mapping, is flexible; if only one node is to be loaded, the iterable level is optional; also each node can be specified as an int, a bytes, a str, in which case non numeric characters are ignored. So this is a legitimate requirement as well:

images_mapping = { 'openair-cn': 12 + 4,
                   'openair-enodeb': ('fit32',),
                   'ubuntu': {12, 'reboot1', '004',
                              'you-get-the-picture-34'}
}

2-Dimension grid

The R2labMap class is a convenience for mapping node numbers to a 2-dimensional grid coordinates, and backwards.

class r2lab.r2labmap.R2labMap[source]

Inherits R2labMapGeneric

A map object where coordinates start at 1, and where the Y coordinate goes upwards; so typically in this map node 1 is at (1, 5) and node 37 is at (9, 1).

class r2lab.r2labmap.R2labMapGeneric(*, offset_x=0, offset_y=0, swap_x=False, swap_y=False, map_x=None, map_y=None)[source]

The most general form allows to specifiy mapper functions in both directions. Or to simply specify an integer offset and boolean swap.

indexes()[source]

Object that can be used to create a pandas index on the nodes; essentially this is range(1, 38)

iterate_holes()[source]

An iterator that yields tuples of the form (x, y) for all the possible (x, y) that do not match a node

iterate_nodes()[source]

An iterator that yields 37 tuples of the form (node_id, (x, y))

node(x, y)[source]

Finds about the node at that position

Parameters:
  • x – coordinate along the horizontal axis - int or str

  • y – coordinate along the vertical axis - int or str

Returns:

a node number, in the range (1..37) - or None

Return type:

int

position(node)[source]

Returns a (x, y) tuple that is the position of node <node>

Parameters:

node – a node number - may be an int or a str

Returns:

a position on the grid

Return type:

(int, int)


Dataframes

A standard pandas DataFrame for storing results on a node by node basis together with their map coordinates.

class r2lab.mapdataframe.MapDataFrame(r2labmap, columns=None)[source]

A MapDataFrame is a dataframe that has one line per node, together with their x and y coordinates as specified in the map object, plus additional columns as specified in the constructor. Is is indexed by node numbers.

Parameters:
  • map – a R2labMap object that primarily provides nodes coordinates

  • columns – an dictionary - preferrably an OrderedDict if using an older Python - that specifies each column name (key) and corresponding initial value (value).


Probing the testbed status

The R2lab sidecar is a websocket service that runs on wss://r2lab.inria.fr:999/ and that exposes the status of the testbed.

This module implements client classes, for interacting with the sidecar service.

The core of the implementation is asyncio-friendly and accessible through the SidecarAsyncClient class, but for convenience some features are also available to synchronous code through the SidecarSyncClient class.

class r2lab.sidecar.SidecarAsyncClient(url='wss://r2lab.inria.fr:999/', *args, **kwds)[source]

This class behaves as an asynchronous context manager for talking with the R2lab sidecar server.

Optional arguments args and kwds are passed as-is to websockets.client.connect, see https://websockets.readthedocs.io/en/stable/api.html#websockets.client.connect

Example

Set a node as available from some asynchronous code:

async with SidecarAsyncClient() as sidecar:
    await sidecar.set_node_attribute(1, 'available', 'ok')

In this example, the sidecar object is a SidecarProtocol instance.

Note

About SSL server certificate verification: Verifying server certificates relies on a set of “trusted” CAs. Web browsers do come with a maintained set of such trust anchors, however the standard Python installation has no such knowledge; and for that reason attempting to check for the testbed’s certificate will fail unless you’ve taken the time to somehow configure all this.

If you just want to probe the testbed though, this looks like a lot of hassle. In that case you can turn off server verification as foolows:

import ssl
ssl_context = ssl.SSLContext()
# this is where we ask for no verification
ssl_context.verify_mode = ssl.CERT_NONE
async with SidecarSyncClient(ssl=ssl_context) as sidecar:
    await sidecar.set_node_attribute(1, 'available', 'ok')
class r2lab.sidecar.SidecarProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header='Python/3.11 websockets/12.0', **kwargs)[source]

The SidecarProtocol class is an asyncio-compliant implementation of the R2lab sidecar system.

It inherits websockets.client.WebSocketClientProtocol as documented here: https://websockets.readthedocs.io/en/stable/api.html#module-websockets.client

async nodes_status()[source]

A function call that returns the JSON nodes status for the complete testbed.

Returns:

A python dictionary indexed by integers 1 to 37, whose values are dictionaries with keys corresponding to each node’s attributes at that time.

Example

Get the complete testbed status:

async with SidecarAsyncClient() as sidecar:
    nodes_status = await sidecar.nodes_status()
print(nodes_status[1]['usrp_type'])
async phones_status()[source]

Just like nodes_status but on phones

async recv_payload()[source]

Receives and returns a SidecarPayload object.

async recv_umbrella()[source]

Read one payload, and returns it as a dict with 3 keys.

async send_payload(payload)[source]

Send a SidecarPayload object.

async send_umbrella(category, action, message)[source]

Set one payload, constructed from its parts

async set_node_attribute(id, attribute, value)[source]
Parameters:
  • id – a node_id as an int or str

  • attribute (str) – the name of the attribute to be written

  • value (str) – the new value

Example

To mark node 1 as unavailable:

await sidecar.set_node_attribute(1, 'available', 'ko')
async set_nodes_triples(*triples)[source]
Parameters:

triples – each argument is expected to be a tuple (or list) of the form id, attribute, value. The same node id can be used in several triples.

Example

To mark node 1 as unavailable and node 2 as turned off:

await sidecar.set_nodes_triples(
    (1, 'available', 'ok'),
    (2, 'cmc_on_off', 'off'),
   )
async set_phone_attribute(id, attribute, value)[source]

Similar to set_node_attribute on a phone

Example

To mark phone 2 as being turned off (although this is constantly recomputed by the phones monitor):

await sidecar.set_phone_attribute(2, 'airplane_mode', 'on')
async set_phones_triples(*triples)[source]

Identical to set_nodes_triples but on phones

class r2lab.sidecar.SidecarSyncClient(url='wss://r2lab.inria.fr:999/', *args, **kwds)[source]

A synchronous wrapper to perform the same operations from sequential code without having to worry about the event loop, asynchronous context manager and coroutine business.

Example

Set a node as available from some synchronous code:

with SidecarSyncClient() as sidecar:
    sidecar.set_node_attribute(1, 'available', 'ok')

Warning

This is a convenience only, it would be unwise, obviously, to call this from asynchronous code; if it works at all. Use SidecarAsyncClient instead in this use case.


Classes for argparse

The classes in this module extend the argparse ecosystem.

Purpose is to enable the creation of CLI options that would behave a bit like action=append, but with a check on choices, that is to say:

  • accumulative : dest holds a list of strings - it’s possible to use the type system as well

  • restrictive : all elements in the list must be in choices

  • optionnally reset-ableit should be possible for add_argument to specify a non-void default

    and in this case we need a means for the CLI to explicitly void the value

In practical terms, we want to specify one or several values for a parameter that is itself constrained, like an antenna mask that must be among ‘1’, ‘3’ and ‘7’

As of this writing at least, using ‘append’ as an action won’t work it is possible to write a code that uses action=append, choices=[‘a’, ‘b’, ‘c’] and default=[‘a’, ‘b’] but then defaults are always returned…

Resetting

The actual syntax offered by your CLI for actually resetting the target list may vary from one need to another

As an example, let us consider a use case where we have 2 physical phones, and we want to be able to select any number of them. Let us further imagine that the code expects that selection to be expressed as a list of integers in the 1-2 range.

So we’d like to say e.g.:

parser.add_argument("-p", "--phones", default=[1], choices=(1, 2), type=int, ...)

And with that in place, we’d like to have

  • no option: results in phones = [1]

  • -p 2: phones = [2]

  • -p 1 -p 2: phones = [1, 2]

  • -p 0: phones = []

Now, it is not possible to adopt a convention where e.g.

  • -p none would mean phones = []

because we have this type = int setting to add_argument, which causes the string "none" to be rejected as an input.

class r2lab.argparse_additions.ListOfChoices(*args, **kwds)[source]

The generic class assumes there is a means_resetting method that is used to check for special incoming values that mean resetting

Example

Not resettable:

parser.add_argument(choices=('1', '2', '3', '4'), default=['1', '2'],
                    typeaction=ListOfChoices)
class r2lab.argparse_additions.ListOfChoicesNullReset(*args, **kwds)[source]

Example

Resettable with -p 0:

parser.add_argument(choices=(1, 2, 3, 0), type=int,
                    default=[1],
                    typeaction=ListOfChoicesNullReset)

Note make sure to mention 0 in the choices.


General utilities

r2lab.utils.find_local_embedded_script(script, extra_paths=None)[source]

This helper is designed to find a script that typically comes with the r2lab-embedded repo, specifically in its shell subdirectory.

It knows of a few heuristics to locate your r2lab-embedded repo, relative to your home and current directories. You can specify additional places to search for in extra_paths

Parameters:
  • script (str) – the simple name of a script to find

  • extra_paths (List(str)) – optional, a list of paths (can be Path instances too) where to search too

Returns:

a valid path in the local filesystem, or None

Return type:

str

Raises:

FileNotFoundError(script)` if script can't be foun

Example

Search for oai-enb.sh so as to run it remotely:

local_script = find_local_embedded_script("oai-enb.sh")
RunScript(localscript, ...)

Note

Should this also look for some env. variable ?

r2lab.utils.r2lab_data(x)[source]

Same as r2lab_hostname but returns an interface name of the form data01.

r2lab.utils.r2lab_hostname(x)[source]

Return a valid hostname like fit01 from an input that can be either 1 (int), 1 (str), 01 (str) , fit1, fit01 or even reboot01.

Parameters:

x (str) – loosely typed input that reflects node number

Examples

Simple use case:

r2lab_hostname(1) == 'fit01'

And:

rl2ab_hostname('reboot1') == 'fit01'
r2lab.utils.r2lab_id(anything)[source]

Returns an integer from an input that can be either 1 (int), 1 (str), b``1``(bytes), 01 (str) , fit1, fit01 or even reboot01.

r2lab.utils.r2lab_parse_slice(slice)[source]

returns username and hostname from a slice.

Parameters:

slice (str) – can be either username@hostname or just username. In the latter case the hostname defaults to the R2lab gateway i.e. faraday.inria.fr

Returns:

slice, hostname

Return type:

tuple

Example

Typical usage is:

slice, hostname = r2lab_parse_slice("inria_r2lab.tutorial")

slice, hostname = r2lab_parse_slice("inria_r2lab.tutorial@faraday.inria.fr")
r2lab.utils.r2lab_reboot(x)[source]

Same as r2lab_hostname but returns a hostname of the form reboot01.