Air SDK V2

The NVIDIA Air SDK V2 provides a Python SDK for interacting with most NVIDIA Air API V2 endpoints.

API V1 and API V2 Overview

The Air API V2 endpoints offer robust methods for creating and managing simulations. Simulations can be initiated through JSON file uploads or by sequentially using RESTful CRUD operations: first by creating a simulation, followed by adding nodes, defining interfaces for each node, and finally linking these interfaces. This structured approach allows for flexible and precise simulation design.

In the Air API V1 endpoints, simulations are structured through a combination of “topology” and “simulation” instances. A simulation comprises a “topology” instance along with a “simulation” instance that references this topology. Individual nodes within the simulation are represented by “node” instances (which reference the topology) and “simulation_node” instances (which reference the simulation). Similarly, each node’s interfaces are represented by “interface” instances (linked to “node” instances) and “simulation_interface” instances (linked to “simulation_node” instances).

With the Air API V2, these representations have been streamlined for the client. A simulation directly contains “nodes,” which in turn contain “interfaces” that connect to one another. The separate concept of a “topology” is almost completely removed, providing a more straightforward structure.

Legacy references to topology Although most API V2 endpoints make no reference to a topology, there are a few exceptions that are included in the API V2 that were created before the implementation and enforcement of the new convention.

Core Endpoints

Key Differences Between V1 and V2

Separate Import Path

The V2 implementation of the SDK has a separate import from the air_sdk package:

from air_sdk import AirApi as AirApiV1  # Imports the original SDK
from air_sdk.v2 import AirApi  # Imports the V2 SDK

api = AirApi(username=..., password=...)

Iterators Versus Lists

Because most API V2 endpoints which list data are paginated, V2 SDK methods often return iterators (e.g., api.simulations.list()) which improve performance but require iteration. Convert iterators to lists when indexing is required:

from air_sdk.v2 import AirApi

api = AirApi(username=..., password=...)

simulation_list = list(api.simulations.list())  # Returns a list of simulation objects

simulation_iterator = api.simulations.list()  # Returns an Iterator

for simulation in simulation_iterator:  # The SDK will walk through the pagination to obtain all objects, potentially across multiple requests
    do_something(simulation)

The SDK uses a default page size of 200. You can adjust the page size to make fewer or more requests by calling list on the iterator:

from air_sdk.v2 import AirApi

api = AirApi(username=..., password=...)
api.set_page_size(10000000)  # Set the page size to be arbitarily large to obtain all objects in one request
sims = list(api.simulations.list())  # will most likely only make 1 call to the Air API

Type Hints and Checks

Most of the SDK V2 comes with type hints that provides assistance and validation when creating or updating objects:

>>> from typing import get_type_hints
>>> for key, value in get_type_hints(air.simulations.create).items():
...    print(key, ':', value)
...
title : <class 'str'>
documentation : typing.Union[str, NoneType]
expires : typing.Union[bool, NoneType]
expires_at : typing.Union[datetime.datetime, NoneType]
metadata : typing.Union[str, NoneType]
organization : typing.Union[air_sdk.v2.endpoints.organizations.Organization, str, uuid.UUID, NoneType]
owner : typing.Union[str, NoneType]
preferred_worker : typing.Union[air_sdk.v2.endpoints.workers.Worker, str, uuid.UUID, NoneType]
sleep : typing.Union[bool, NoneType]
sleep_at : typing.Union[datetime.datetime, NoneType]
return : <class 'air_sdk.v2.endpoints.simulations.Simulation'>

Set Custom Connection Timeouts

Clients may set a custom connection timeout for the SDK V2:

from datetime import timedelta
from air_sdk.v2 import AirApi

api = AirApi(...)

api.set_connect_timeout(timedelta(minutes=2))

A custom read timeout may be set separately:

api.set_read_timeout(timedelta(minutes=2))

Additional Authentication Support

The initialization process for the original and the V2 SDK is nearly identical:

from air_sdk import AirApi as AirApiV1
from air_sdk.v2 import AirApi as AirApiV2

air_v1 = AirApiV1(
    api_url=...,
    username=...,
    password=...,
)
air_v2 = AirApiV2(
    api_url=...,
    username=...,
    password=...,
)

However, there is an additional option to skip authentication during the initialization of the SDK V2 and provide authentication credentials at a later time:

from air_sdk.v2 import AirApi
api = AirApi(api_url=..., authenticate=False)
api.client.authenticate(username=..., password=...)

This method can also be used to switch which client is authenticated.

Interact with Dataclass Objects

SDK V2 introduces dataclasses for representing various objects like simulations, nodes, images, and organizations in Python.

>>> sim
Simulation(id='95bbbf37-a6d4-42b2-ab62-0234cc86370d', title='2k links', state='NEW', documentation=None, write_ok=True, metadata=None)
>>> sim.id
'95bbbf37-a6d4-42b2-ab62-0234cc86370d'
>>> sim.title
'2k links'
>>> sim.created
datetime.datetime(2024, 10, 18, 16, 11, 12, 659424, tzinfo=datetime.timezone.utc)

You can easily convert these objects to native Python dictionaries using the .dict() method:

>>> sim.dict()
{'id': '95bbbf37-a6d4-42b2-ab62-0234cc86370d', 'title': '2k links', 'state': 'NEW', 'sleep': True, 'owner': 'tiparker@nvidia.com', 'cloned': False, 'expires': False, 'created': datetime.datetime(2024, 10, 18, 16, 11, 12, 659424, tzinfo=datetime.timezone.utc), 'modified': datetime.datetime(2024, 10, 31, 17, 50, 28, 905146, tzinfo=datetime.timezone.utc), 'sleep_at': datetime.datetime(2024, 10, 19, 4, 11, 12, 649304, tzinfo=datetime.timezone.utc), 'expires_at': datetime.datetime(2024, 11, 1, 16, 11, 12, 649000, tzinfo=datetime.timezone.utc), 'organization': '3b7c20c9-e525-46ac-96e3-a9a332aef774', 'preferred_worker': None, 'documentation': None, 'write_ok': True, 'metadata': None}

To convert to a JSON string, use the .json() method:

>>> sim.json()
'{"id":"95bbbf37-a6d4-42b2-ab62-0234cc86370d","title":"2k links","state":"NEW","sleep":true,"owner":"tiparker@nvidia.com","cloned":false,"expires":false,"created":"2024-10-18T16:11:12.659424Z","modified":"2024-10-31T17:50:28.905146Z","sleep_at":"2024-10-19T04:11:12.649304Z","expires_at":"2024-11-01T16:11:12.649000Z","organization":"3b7c20c9-e525-46ac-96e3-a9a332aef774","preferred_worker":null,"documentation":null,"write_ok":true,"metadata":null}'

To synchronize an object’s data with the latest API state, use the .refresh() method:

>>> sim.title
'2k links'
>>> sim.title = 'New Name'
>>> sim.title
'New Name'
>>> sim.refresh() # Refreshes the data from the API
>>> sim.title
'2k links'

As seen when calling .json() or .dict() above, Simulation instances may reference an associated organization.

It is often possible to directly access related objects. For example:

>>> sim.organization
Organization(id='3b7c20c9-e525-46ac-96e3-a9a332aef774', name='Tim test org', member_count=8)

These related objects are created lazily, meaning the Organization object is fetched on-demand when accessed for the first time. This allows seamless traversal of relationships between connected objects:

>>> sim
Simulation(id='95bbbf37-a6d4-42b2-ab62-0234cc86370d', title='2k links', state='NEW', documentation=None, write_ok=True, metadata=None)
>>> sim.organization
Organization(id='3b7c20c9-e525-46ac-96e3-a9a332aef774', name='Tim test org', member_count=8)
>>> sim.organization.dict()
{
    'id': '3b7c20c9-e525-46ac-96e3-a9a332aef774',
    'name': 'Tim test org',
    'member_count': 8,
    'resource_budget': 'b0c2a464-f6c5-4a9c-a65c-d8645d6fa01f'
}
>>> sim.organization.resource_budget
ResourceBudget(id='b0c2a464-f6c5-4a9c-a65c-d8645d6fa01f')
>>> sim.organization.resource_budget.dict()
{
    'id': 'b0c2a464-f6c5-4a9c-a65c-d8645d6fa01f',
    'cpu': 300,
    'cpu_used': 0,
    'image_uploads': 10000000000,
    'image_uploads_used': 111804416,
    'memory': 300000,
    'memory_used': 0,
    'simulations': 15,
    'simulations_used': 0,
    'storage': 3000,
    'storage_used': 0,
    'userconfigs': 10,
    'userconfigs_used': 0
}

When comparing objects accessed by different processes, you should compare the object’s id (or other primary key):

>>> id(sim) == id(node.simulation)  # Different objects in Python
False
>>> sim == node.simulation
False
>>> sim.id == node.simulation.id
True

In Air, simulations are structured with multiple nodes, and each node can contain several interfaces. In the SDK V2, these ‘many-to-one’ relationships—where a simulation contains many nodes, and a node contains multiple interfaces—must be explicitly queried to access all related entities. For example:

from air_sdk.v2 import AirApi

air = AirApi(username=..., password=...)

```python
>>> sim = air.simulations.get('1ebf9958-a01e-4396-88f6-946e93299cf2')
>>> hasattr(sim, 'nodes')
False

Iterate through a list of nodes for a sim:

>>> for node in air.nodes.list(simulation=sim):
...    print(node.name)
... 
oob-mgmt-switch
node7
node2
node3
node4
node1
node6
node10
oob-mgmt-server
node8
node5
node9

Alternatively, obtain a list of nodes by calling list:

>>> nodes = list(air.nodes.list(simulation=sim))
>>> len(nodes)
12

Interfaces can be filtered by individual nodes or by simulations:

>>> sim = air.simulations.get('<simulation-id>')
>>> node = next(air.nodes.list(simulation=sim))
>>> node_interfaces = list(air.interfaces.list(node=node))
>>> len(node_interfaces)
1
>>> sim_interfaces = list(air.interfaces.list(simulation=node.simulation))
>>> len(sim_interfaces)
29

Create a Simulation

There are two paths for creating simulations using the SDK V2:

  • File import
  • Blank simulation creation

File Import

Entire simulations can efficiently and reliably be created by importing a file. This process is similar to the DOT file upload process supported by the original SDK and mirrors the simulation import endpoint.

More details can be found in the import instructions section of the quick start guide.

from air_sdk.v2 import AirApi

air = AirApi(username=..., password=...)

simulation = air.simulations.create_from(
    'my-simulation',  # The Title
    'JSON',  # The format of the content. Only JSON is supported currently.
    {
        'nodes': {
            'node-1': {
                'os': 'generic/ubuntu2204',
            },
            'node-2': {
                'os': 'generic/ubuntu2204',
            },
        },
        'links': [
            [{'node': 'node-1', 'interface': 'eth1'}, {'node': 'node-2', 'interface': 'eth1'}]
        ]
    },
)

Create a Blank Simulation

A blank simulation (that is, a simulation with no nodes) may be created via the basic create simulation endpoint.

from air_sdk.v2 import AirApi

air = AirApi(username=..., password=...)

personal_sim = air.simulations.create(title="Blank Simulation for myself")

org = next(air.organizations.list(search="My Favorite Organization"))
sim_for_my_org = air.simulations.create(
    title="Blank Simulation for my Favorite Org",
    organization=org,
)

Most fields specified by the create simulation endpoint can be passed into the air.simulations.create method.

Modify a Simulation

Existing simulations can be customized by adjusting their fields, adding or removing nodes, and updating node interfaces. New interfaces can be added or removed from nodes and connected as needed.

Adjust the Fields on a Simulation Object

Select an Existing Simulation

An existing simulation can be retrieved by its id:

from air_sdk.v2 import AirApi

air = AirApi(username=..., password=...)

simulation = air.simulations.get('<simulation-id>')

Alternatively, a simulation can be queried for by the list simulations endpoint:

simulation = next(air.simulations.list(title="My Simulation Title"))  # using `next` gets the first result

Simulations can be queried by any of the values specified in list simulations.

my_favorite_org = next(air.organizations.list(search="My Favorite Org"))  # using `next` returns the first result
simulation = next(air.simulations.list(title="My Simulation's Title", organization=my_favorite_org))

Update an Existing Simulation

Update specific fields by calling .update:

>>> sim = air.simulations.get('1ebf9958-a01e-4396-88f6-946e93299cf2')
>>> sim.title
'Sam Personal 10 w OOB'
>>> sim.update(title="Sam's Personal 10 node sim with OOB")
>>> sim.title
"Sam's Personal 10 node sim with OOB"

Calling .update on a simulation objects corresponds to PATCH simulation V2.

There is also a .full_update method on the simulation which updates all fields on the simulation:

sim.full_update(
    title='New Title',
    documentation=sim.documentation,
    expires=sim.expires,
    expires_at=sim.expires_at,
    metadata=sim.metadata,
    preferred_worker=sim.preferred_worker,
    sleep=sim.sleep,
    sleep_at=sim.sleep_at,
)

Node and Interface objects have similar .update and .full_update methods for modifying their data.

Add New Nodes to a Simulation

>>> image = next(air.images.list(name='generic/ubuntu2204'))  # Obtain an image for the node
>>> image
Image(name='generic/ubuntu2204', version='22.04', organization_name=None)
>>> new_node = air.nodes.create(simulation=sim, name='node13', os=image)
>>> new_node.os.id == image.id
True
>>> new_node.simulation.id == sim.id
True

Export a Simulation

Existing simulations can be exported into a JSON representation which can be shared and re-imported into Air.

from air_sdk.v2 import AirApi

air = AirApi(username=..., password=...)

sim_export_json = air.simulations.export(
    simulation='<simulation-id>',
    format='JSON',
    image_ids=True,  # defaults to False
)

# Or call `export` on a simulation object
simulation = air.simulations.get('<simulation-id>')

sim_export_json = simulation.export(format="JSON")

For more information, refer to the export instructions section in the quick start guide.