The r2lab API — r2lab v0.6.2

# 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)

  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

  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)

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

  indexes()

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

  iterate_holes()

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

  iterate_nodes()

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

  node(x, y)

    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)

    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)

  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)

  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)

  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()

    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()

    Just like `nodes_status` but on phones

  async recv_payload()

    Receives and returns a `SidecarPayload` object.

  async recv_umbrella()

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

  async send_payload(payload)

    Send a `SidecarPayload` object.

  async send_umbrella(category, action, message)

    Set one payload, constructed from its parts

  async set_node_attribute(id, attribute, value)
    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)
    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)

    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)

    Identical to `set_nodes_triples` but on phones

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

  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)

  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)

  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)

  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)

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

r2lab.utils.r2lab_hostname(x)

  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)

  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)

  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)

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