Internos do SDK¶
Esta página explica como o netbox_sdk funciona internamente — o ciclo de vida do cliente HTTP, o sistema de configuração e perfis, a indexação do esquema OpenAPI, a hierarquia de objetos da fachada, clientes tipados por versão, o cache HTTP em disco e a camada de serviços.
Ciclo de vida das requisições do NetBoxApiClient¶
NetBoxApiClient em netbox_sdk/client.py é o cliente HTTP async central. Toda requisição passa pelo mesmo pipeline, independentemente de qual camada de API (raw, facade ou typed) a iniciou.
sequenceDiagram
participant Caller as Código chamador
participant Client as NetBoxApiClient
participant Cache as HttpCacheStore
participant Session as aiohttp.ClientSession
participant NB as NetBox API
Caller->>Client: request("GET", "/api/dcim/devices/", query={...})
Client->>Client: _normalize_request_path() — proteção SSRF
Client->>Client: authorization_header_value() — montar cabeçalho de auth
Client->>Client: _cache_policy() — determinar política de TTL
Client->>Cache: load(cache_key)
alt Cache FRESCO (dentro do TTL)
Cache-->>Client: CacheEntry
Client-->>Caller: ApiResponse (X-NBX-Cache: HIT)
else Cache EXPIRADO ou MISS
alt Entrada expirada existe
Client->>Client: adicionar If-None-Match / If-Modified-Since
end
Client->>Client: _get_session() — sessão aiohttp lazy
Client->>Session: session.request(GET, url, ...)
Session->>NB: HTTPS GET
NB-->>Session: 200 {...} ou 304 Not Modified
Session-->>Client: resposta bruta
alt 401/403 com token v2
Client->>Session: retentar com cabeçalho Token fallback
end
Client->>Cache: save(key, entry, policy)
Client-->>Caller: ApiResponse
end
Criação lazy de sessão¶
A aiohttp.ClientSession é criada na primeira requisição e reutilizada em todas as chamadas subsequentes. Um padrão de lock com dupla verificação lida com a afinidade de loop de eventos:
async def _get_session(self) -> aiohttp.ClientSession:
current_loop_id = id(asyncio.get_running_loop())
# Caminho rápido: sessão já válida para este loop — sem lock necessário
if (
self._session is not None
and not self._session_closed()
and self._session_loop_id == current_loop_id
):
return self._session
async with self._get_lock():
# Reverificar sob lock caso outra coroutine acabou de criar a sessão
if self._session is None or session_closed or self._session_loop_id != current_loop_id:
...
self._session = aiohttp.ClientSession(timeout=..., connector=...)
self._session_loop_id = current_loop_id
return self._session
Proteção SSRF¶
Todos os caminhos de requisição passam por _normalize_request_path(), que rejeita URLs absolutas, query strings e fragmentos embutidos no argumento de caminho:
def _normalize_request_path(self, path: str) -> str:
parsed = urlsplit(path.strip())
if parsed.scheme or parsed.netloc:
raise ValueError("O caminho da requisição deve ser relativo à URL base configurada")
if parsed.query or parsed.fragment:
raise ValueError("O caminho da requisição não deve incluir parâmetros de query ou fragmentos")
return parsed.path if parsed.path.startswith("/") else f"/{parsed.path}"
Fallback de token v2 para v1¶
Quando um token v2 nbt_ recebe 401/403 com "invalid v2 token" no corpo, o cliente retenta transparentemente com um cabeçalho v1 Token <secret>:
def _should_retry_with_v1(self, response: ApiResponse) -> bool:
if self.config.token_version != "v2" or not self.config.token_secret:
return False
if response.status not in {401, 403}:
return False
return "invalid v2 token" in response.text.lower()
Sistema de configuração e perfis¶
Config em netbox_sdk/config.py é um modelo Pydantic que normaliza e valida parâmetros de conexão antes de passá-los ao NetBoxApiClient.
Campos e validadores¶
| Campo | Tipo | Descrição |
|---|---|---|
base_url |
str \| None |
URL base do NetBox — normalizada para http:// ou https:// apenas |
token_version |
str |
"v1" (legacy Token) ou "v2" (bearer nbt_) |
token_key |
str \| None |
Prefixo de chave de token v2 (antes de .) |
token_secret |
str \| None |
Valor do token — CR/LF/null removidos para evitar injeção de cabeçalho |
timeout |
float |
Timeout HTTP em segundos (padrão: 30.0) |
ssl_verify |
bool |
Verificação de certificado TLS (padrão: True) |
demo_username |
str \| None |
Usuário para login automático no perfil de demonstração |
demo_password |
str \| None |
Senha para login automático no perfil de demonstração |
Persistência de múltiplos perfis¶
Perfis são armazenados como {"profiles": {"default": {...}, "demo": {...}}} em ~/.config/netbox-sdk/config.json com permissões 0o600:
# Carregar o perfil ativo
config = load_profile_config(profile="default")
# Salvar credenciais atualizadas
save_config(config, profile="default")
Substituição por variáveis de ambiente¶
| Variável | Campo Config |
|---|---|
NETBOX_URL |
base_url |
NETBOX_TOKEN_KEY |
token_key |
NETBOX_TOKEN_SECRET |
token_secret |
NETBOX_SSL_VERIFY |
ssl_verify |
DEMO_USERNAME |
demo_username |
DEMO_PASSWORD |
demo_password |
SchemaIndex (Parsing de OpenAPI)¶
SchemaIndex em netbox_sdk/schema.py analisa o JSON OpenAPI fornecido em um índice em memória otimizado para consultas rápidas de grupo/recurso/operação.
flowchart LR
JSON["netbox-openapi-4.5.json\n(embutido)"]
BUILD["SchemaIndex._build()\nanalisar todos os caminhos"]
OPS["_operations\nlist[Operation]"]
RPATHS["_resource_paths\ndict[grupo+recurso → ResourcePaths]"]
GROUPS["groups() → list[str]"]
RES["resources(group) → list[str]"]
OPFOR["operations_for(group, resource) → list[Operation]"]
FILTER["filter_params(group, resource) → list[FilterParam]"]
JSON --> BUILD
BUILD --> OPS
BUILD --> RPATHS
OPS --> GROUPS
OPS --> RES
OPS --> OPFOR
RPATHS --> FILTER
Esquemas embutidos por versão¶
Cinco esquemas OpenAPI são fornecidos com o pacote em netbox_sdk/reference/openapi/:
| Arquivo | Versão do NetBox |
|---|---|
netbox-openapi.json |
Padrão (mais recente) |
netbox-openapi-4.6.json |
NetBox 4.6 |
netbox-openapi-4.5.json |
NetBox 4.5 |
netbox-openapi-4.4.json |
NetBox 4.4 |
netbox-openapi-4.3.json |
NetBox 4.3 |
Hierarquia de objetos da fachada¶
netbox_sdk/facade.py fornece uma API async compatível com PyNetBox. A função api() constrói o objeto Api raiz; acessos de atributo subsequentes criam App → Endpoint → Record / RecordSet.
flowchart TB
APIFN["api(url, token)\nfunção factory"]
API["Api\n.client · .schema · .dcim · .ipam · …"]
APP["App('dcim')\n__getattr__ → Endpoint"]
ENDPOINT["Endpoint('devices')\n.all() · .filter() · .get() · .create()"]
RECORDSET["RecordSet\niterador async · paginação automática"]
RECORD["Record\nacesso por atributo · rastreamento dirty\n.save() · .delete()"]
APIFN --> API
API -->|".dcim"| APP
APP -->|".devices"| ENDPOINT
ENDPOINT -->|".all()"| RECORDSET
RECORDSET -->|"async for item in …"| RECORD
ENDPOINT -->|".get(id)"| RECORD
ENDPOINT -->|".create(payload)"| RECORD
Operações CRUD¶
nb = api("https://netbox.example.com", token="...")
# Listar todos — iteração async com paginação automática
async for device in nb.dcim.devices.all():
print(device.name, device.status)
# Filtrar com parâmetros de query
records = nb.dcim.devices.filter(site="nyc-dc1", status="active")
async for device in records:
print(device)
# Obter por ID
device = await nb.dcim.devices.get(42)
# Criar
tag = await nb.extras.tags.create({
"name": "proxmox",
"slug": "proxmox",
"color": "ff5722",
})
Rastreamento dirty em Record¶
Record captura um snapshot dos valores de campo na criação. Mutar um campo o adiciona a um dict _updates; chamar .save() envia apenas os campos alterados como PATCH:
device = await nb.dcim.devices.get(42)
device.status = "offline" # marca "status" como dirty
device.comments = "decommissioned" # marca "comments" como dirty
await device.save() # PATCH {status, comments} apenas
API Tipada (Clientes por versão)¶
typed_api() em netbox_sdk/typed_api.py retorna um cliente tipado por versão, respaldado por modelos Pydantic gerados para completação completa em IDE e validação em runtime.
from netbox_sdk import typed_api
nb = typed_api("https://netbox.example.com", token="...", netbox_version="4.5")
# Validação Pydantic completa em request e response
device = await nb.dcim.devices.retrieve(42)
device.name # str — a IDE conhece o tipo
device.status # enum DeviceStatus
Cache HTTP¶
HttpCacheStore em netbox_sdk/http_cache.py fornece um cache JSON baseado em disco armazenado em ~/.config/netbox-sdk/http-cache/.
flowchart TD
REQ["Requisição GET entrante"]
KEY["build_cache_key()\nSHA-256 de base_url + método + caminho + query + fingerprint do token"]
LOAD["HttpCacheStore.load(key)"]
FRESH{"Entrada existe\ne está fresca?"}
STALE{"Entrada existe\nmas expirada?"}
COND["Adicionar If-None-Match /\nIf-Modified-Since"]
HTTP["Fazer requisição HTTP"]
STATUS{"Status da resposta"}
S304["304 Not Modified\n→ atualizar timestamps"]
S2XX["2xx → salvar nova entrada"]
S5XX["Erro 5xx\n→ servir expirado se dentro de\nstale_if_error_until"]
RETURN["Retornar ApiResponse\n(cabeçalho X-NBX-Cache)"]
REQ --> KEY --> LOAD --> FRESH
FRESH -->|Sim| RETURN
FRESH -->|Não| STALE
STALE -->|Sim| COND --> HTTP
STALE -->|Não| HTTP
HTTP --> STATUS
STATUS --> S304 --> RETURN
STATUS --> S2XX --> RETURN
STATUS --> S5XX --> RETURN
Políticas de cache¶
| Tipo de requisição | TTL fresco | TTL stale-if-error |
|---|---|---|
GET lista (ex.: /api/dcim/devices/) |
60 s | 300 s |
| GET detalhe com query | 30 s | 60 s |
| GET detalhe sem query | 15 s | 60 s |
| Não-GET (POST/PUT/PATCH/DELETE) | Não cacheado | — |
Camada de serviços¶
netbox_sdk/services.py mapeia nomes de ação voltados ao usuário para chamadas HTTP, fazendo a ponte entre o CLI e o cliente HTTP.
ACTION_METHOD_MAP¶
| Ação | Método HTTP | Caminho |
|---|---|---|
list |
GET | list_path |
get |
GET | detail_path (requer --id) |
create |
POST | list_path |
update |
PUT | detail_path (requer --id) |
patch |
PATCH | detail_path (requer --id) |
delete |
DELETE | detail_path (requer --id) |
resolve_dynamic_request()¶
Recebe uma tupla (grupo, recurso, ação, id, query_params, payload) e retorna um ResolvedRequest(método, caminho, query, payload):
class ResolvedRequest(BaseModel):
method: str
path: str
query: dict[str, str]
payload: dict[str, Any] | list[Any] | None
run_dynamic_command()¶
Combina resolve_dynamic_request() com um NetBoxApiClient para executar a requisição completa de ponta a ponta. Usado por netbox_cli/dynamic.py para alimentar todos os comandos nbx <grupo> <recurso> <ação> gerados na inicialização a partir do esquema OpenAPI.