Air SDK V2

NVIDIA Air SDK V2 提供了一个 Python SDK,用于与大多数 NVIDIA Air API V2 端点进行交互。

API V1 和 API V2 概述

Air API V2 端点提供了用于创建和管理模拟的强大方法。您可以通过 JSON 文件上传或按顺序使用 RESTful CRUD 操作来启动模拟:首先创建模拟,然后添加节点,为每个节点定义接口,最后链接这些接口。这种结构化方法允许灵活和精确的模拟设计。

在 Air API V1 端点中,模拟通过“拓扑”和“模拟”实例的组合进行结构化。一个模拟包含一个“拓扑”实例以及一个引用此拓扑的“模拟”实例。

模拟中的各个节点由“节点”实例(引用拓扑)和“simulation_node”实例(引用模拟)表示。每个节点的接口由“接口”实例(链接到“节点”实例)和“simulation_interface”实例(链接到“simulation_node”实例)表示。

使用 Air API V2,这些表示形式为客户端进行了简化。一个模拟直接包含“节点”,而“节点”又包含彼此连接的“接口”。“拓扑”的单独概念几乎完全被移除,从而提供了更直接的结构。

拓扑的遗留引用尽管大多数 API V2 端点不引用拓扑,但 API V2 中仍包含一些例外情况,这些例外情况是在新约定实施和执行之前创建的。

核心端点

V1 和 V2 之间的主要区别

单独的导入路径

SDK 的 V2 实现与 air_sdk 包的导入路径不同

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=...)

迭代器与列表

由于大多数列出数据的 API V2 端点都已分页,因此 V2 SDK 方法通常返回迭代器(例如,api.simulations.list()),这提高了性能,但需要迭代。在需要索引时,将迭代器转换为列表

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)

SDK 使用默认页面大小 200。您可以通过在迭代器上调用 list 来调整页面大小,以减少或增加请求次数

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

类型提示和检查

大多数 SDK V2 都带有类型提示,可在创建或更新对象时提供帮助和验证

>>> 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'>

设置自定义连接超时

客户端可以为 SDK V2 设置自定义连接超时

from datetime import timedelta
from air_sdk.v2 import AirApi

api = AirApi(...)

api.set_connect_timeout(timedelta(minutes=2))

可以单独设置自定义读取超时

api.set_read_timeout(timedelta(minutes=2))

额外的身份验证支持

原始 SDK 和 V2 SDK 的初始化过程几乎相同

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=...,
)

还有一个额外的选项可以在 SDK V2 的初始化期间跳过身份验证,并在稍后提供身份验证凭据

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

您还可以使用此方法来切换经过身份验证的客户端。

与数据类对象交互

SDK V2 引入了数据类,用于在 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)

您可以使用 .dict() 方法轻松地将这些对象转换为原生 Python 字典

>>> 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}

要转换为 JSON 字符串,请使用 .json() 方法

>>> 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}'

要将对象的数据与最新的 API 状态同步,请使用 .refresh() 方法

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

如上文调用 .json().dict() 时所见,Simulation 实例可能引用关联的 organization

通常可以直接访问相关对象。例如

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

这些相关对象是延迟创建的,这意味着 Organization 对象在首次访问时按需获取。这允许在连接对象之间无缝遍历关系

>>> 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
}

当比较不同进程访问的对象时,您应该比较对象的 id(或其他主键)

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

在 Air 中,模拟由多个节点构成,每个节点可以包含多个接口。在 SDK V2 中,这些“多对一”关系(即一个模拟包含多个节点,一个节点包含多个接口)必须显式查询才能访问所有相关实体。例如

from air_sdk.v2 import AirApi

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

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

迭代模拟的节点列表

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

或者,通过调用 list 获取节点列表

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

接口可以通过单个节点或模拟进行筛选

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

创建模拟

使用 SDK V2 创建模拟有两种途径

  • 文件导入
  • 创建空白模拟

文件导入

您可以通过导入文件来高效可靠地创建完整的模拟。此过程类似于原始 SDK 支持的 DOT 文件上传过程,并镜像了 simulation import 端点。

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 simulation 端点来创建空白模拟(即没有节点的模拟)。

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

创建模拟端点指定的大多数字段都可以传递到 air.simulations.create 方法中。

修改模拟

您可以通过调整字段、添加或删除节点以及更新节点接口来自定义现有模拟。您可以根据需要从节点添加或删除新接口并连接它们。

调整模拟对象上的字段

选择现有模拟

您可以使用其 ID 检索现有模拟

from air_sdk.v2 import AirApi

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

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

或者,您可以通过 list simulations 端点查询模拟

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

您可以通过 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 更新特定字段

>>> 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"

在模拟对象上调用 .update 对应于 PATCH simulation V2

模拟上还有一个 .full_update 方法,用于更新模拟上的所有字段

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

NodeInterface 对象具有类似的 .update.full_update 方法来修改其数据。

向模拟添加新节点

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

导出模拟

您可以将现有模拟导出为 JSON 表示形式,您可以共享该表示形式并将其重新导入到 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")