Nodes

View as Markdown
# Imports (run once)
from air_sdk import AirApi
from air_sdk.endpoints.images import Image
from air_sdk.endpoints.nodes import Node
from air_sdk.endpoints.simulations import Simulation
# Authentication (run once)
api = AirApi.with_ngc_config()
# OR api = AirApi.with_api_key(api_key="...")
# OR api = AirApi.with_device_login(email="...", org_num="...")
# ^ use in terminal only — not supported in Jupyter notebooks

Create

We can create new nodes for a simulation by using the api.nodes.create method:

sim: Simulation = api.simulations.create(name='Blank Simulation')
ubuntu_2204 = next(api.images.list(search='generic/ubuntu2204'))
node: Node = api.nodes.create(
simulation=sim,
image=ubuntu_2204,
name='ubuntu-server',
)
node.dict()
{'id': 'a06bad49-40bb-4ded-a24c-5f182a0cdba0',
'created': datetime.datetime(2026, 7, 22, 9, 40, 8, 532132, tzinfo=datetime.timezone.utc),
'modified': datetime.datetime(2026, 7, 22, 9, 40, 8, 532153, tzinfo=datetime.timezone.utc),
'name': 'ubuntu-server',
'simulation': '98b8806b-f1fa-442a-9ff5-d642853842fc',
'image': '8674741d-5bdf-417f-bc78-7bda9ad4d101',
'category': '',
'state': 'INACTIVE',
'status_from_worker': '',
'split_options': None,
'cpu': 1,
'memory': 1024,
'storage': 10,
'pos_x': 0,
'pos_y': 0,
'cdrom': None,
'storage_pci': None,
'labels': {},
'metadata': '{"mgmt_mac": "48:B0:2D:00:00:00"}',
'advanced': {'nic_model': 'virtio',
'cpu_mode': 'custom',
'cpu_options': [],
'secureboot': False,
'uefi': False,
'boot': 'hd'},
'management_interfaces': {'eth0': {'ip': None,
'mac_address': '48:B0:2D:00:00:00'}}}

Nodes can also be created off of an existing Simulation instance:

sim: Simulation = api.simulations.create(name='Blank Simulation')
ubuntu_2204 = next(api.images.list(search='generic/ubuntu2204'))
# You do not have to specify `simulation=sim` in the payload when using this method.
node: Node = sim.nodes.create(name='new-node', image=ubuntu_2204)
node.dict()
{'id': '04f8e68a-6bb1-4de9-be1b-6e5a457084ca',
'created': datetime.datetime(2026, 7, 22, 9, 37, 53, 946807, tzinfo=datetime.timezone.utc),
'modified': datetime.datetime(2026, 7, 22, 9, 37, 53, 946827, tzinfo=datetime.timezone.utc),
'name': 'new-node',
'simulation': '59b873b9-e94e-460a-914e-1233e86807ab',
'image': '8674741d-5bdf-417f-bc78-7bda9ad4d101',
'category': '',
'state': 'INACTIVE',
'status_from_worker': '',
'split_options': None,
'cpu': 1,
'memory': 1024,
'storage': 10,
'pos_x': 0,
'pos_y': 0,
'cdrom': None,
'storage_pci': None,
'labels': {},
'metadata': '{"mgmt_mac": "48:B0:2D:00:00:00"}',
'advanced': {'nic_model': 'virtio',
'cpu_mode': 'custom',
'cpu_options': [],
'secureboot': False,
'uefi': False,
'boot': 'hd'},
'management_interfaces': {'eth0': {'ip': None,
'mac_address': '48:B0:2D:00:00:00'}}}

Get

How to list, filter, order, and search for nodes of a simulation.

ubuntu_2204: Image = next(api.images.list(search='generic/ubuntu2204'))
cumulus_5_6_0: Image = next(api.images.list(search='cumulus-vx-5.6.0'))
sim: Simulation = api.simulations.create(name='Blank Simulation')
# Create nodes with various specifications
sim.nodes.create(name='ubuntu-1', image=ubuntu_2204)
sim.nodes.create(name='ubuntu-2', image=ubuntu_2204)
sim.nodes.create(name='ubuntu-12', image=ubuntu_2204)
sim.nodes.create(name='special-node', image=ubuntu_2204)
sim.nodes.create(name='node14', image=cumulus_5_6_0)
sim.nodes.create(name='node15', image=cumulus_5_6_0)
sim.nodes.create(name='node16', image=cumulus_5_6_0)
for node in sim.nodes.list():
print(node)
Node(id='42917bde-000a-4209-a20b-59db0c15b1a1', name='ubuntu-1')
Node(id='552b72bd-e4c5-4226-b50e-0e763c7890b8', name='ubuntu-2')
Node(id='c2903bfe-fa0b-43ec-ae75-7faf80f1f049', name='ubuntu-12')
Node(id='7f6772aa-76ef-4012-a2e0-e6d30ceb78a4', name='special-node')
Node(id='f61db084-99da-4928-bdb3-7bbd5950416e', name='node14')
Node(id='1a77d4bd-f533-48a7-a169-1415d52d8d91', name='node15')
Node(id='4abe2036-b4d1-438e-ad3b-9c236fd32e47', name='node16')

Order Nodes by Specific Fields

for node in sim.nodes.list(ordering='name'):
print(node.name)
node14
node15
node16
special-node
ubuntu-1
ubuntu-12
ubuntu-2

Search for Nodes by Name

for node in sim.nodes.list(search='node'):
print(node.name)
node14
node15
node16

Filter Node by Specific Properties

for node in sim.nodes.list(image=cumulus_5_6_0):
print(node.name, node.image.name)
node14 cumulus-vx-5.6.0
node15 cumulus-vx-5.6.0
node16 cumulus-vx-5.6.0

This can be used to obtain a specific node by name

list(sim.nodes.list(name='special-node'))
[Node(id='7f6772aa-76ef-4012-a2e0-e6d30ceb78a4', name='special-node')]

Placement Groups

On an Air API deployment that supports placement groups, nodes with the same non-empty placement_group in one simulation are placed together on a worker. Names are case-sensitive, surrounding whitespace is trimmed, and the maximum length is 64 characters. This is separate from visual grouping through labels.group.

Use an INACTIVE simulation with no checkpoints from the CREATE examples above.

grouped_node = api.nodes.create(
simulation=sim,
image=ubuntu_2204,
name='rack-a-server-1',
placement_group='rack-a',
)
api.nodes.create(
simulation=sim,
image=ubuntu_2204,
name='rack-a-server-2',
placement_group='rack-a',
)
# Membership is available in both detail and list results.
print(api.nodes.get(grouped_node.id).placement_group)
for member in api.nodes.list(simulation=sim.id):
print(member.name, member.placement_group)
rack-a
rack-a-server-1 rack-a
rack-a-server-2 rack-a

Omit placement_group from an update to retain membership. Pass a new string to change membership, or '' to remove it. None is sent as JSON null and is rejected by the API; it does not clear membership. Update a generated child’s parent to change their shared membership. Edits require an INACTIVE simulation without checkpoints.

grouped_node.update(name='renamed-rack-server') # Retains rack-a.
print('After rename:', grouped_node.placement_group)
grouped_node.update(placement_group='rack-b') # Moves this node to rack-b.
print('After reassignment:', grouped_node.placement_group)
grouped_node.update(placement_group='') # Removes this node from its group.
print('After removal:', repr(grouped_node.placement_group))
After rename: rack-a
After reassignment: rack-b
After removal: ''