././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9693656 pynetbox-7.8.0/0000755000175100017510000000000015215017112013037 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9427743 pynetbox-7.8.0/.claude/0000755000175100017510000000000015215017112014352 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.claude/README.md0000644000175100017510000000516715215017101015640 0ustar00runnerrunner# .claude/ Project-local Claude Code configuration for pynetbox. The tool-agnostic content layer for this repo is [`AGENTS.md`](../AGENTS.md) at the repo root, with its `CLAUDE.md` shim. This `.claude/` directory is the Claude-specific action layer that complements `AGENTS.md` with project-local skills, slash commands, and per-developer settings. ## Layout - `skills/` — Project-local Claude Code skills. Each skill is its own subdirectory containing a `SKILL.md` describing what it does and when to use it. Use this for repo-specific procedures. - `commands/` — Project-local slash commands. One Markdown file per command: `commands/.md`. Use this for `/foo` shortcuts that only make sense in this repo. - `settings.local.json` — Per-developer Claude Code settings (tool permissions, MCP server paths, IDE preferences). **Never committed** — this filename is in the repo's `.gitignore`. ## When to add a skill (vs. inlining in AGENTS.md or promoting upstream) Add a skill here when: - The procedure is repo-specific (it would not be useful in other NBL repos as-is). - The procedure is non-trivial (more than a one-line note that fits naturally inside `AGENTS.md`). - The procedure is a recipe an agent or engineer might re-run, not a one-off. ## When to add a slash command Add a command here when: - The action is something you find yourself typing the same prompt for repeatedly. - The repo has a non-obvious workflow that benefits from a shortcut. ## Conventions - Skill and command names use `lowercase-kebab-case`, matching the [folder naming convention in `AGENTS.md`](../AGENTS.md). - Each skill directory has a `SKILL.md` (the entry point); supporting files (references, examples, sample data) live alongside it inside the skill's directory. - Each command is a single Markdown file named for the slash command: `commands/.md`. - Skills and commands document *why* they make the choices they do — the rationale is more durable than the bare instruction. ## How to add your first skill 1. Pick a kebab-case name describing the action: e.g., `parse-linear-issues`, `render-delivery-row`. 2. `mkdir .claude/skills//` and create `SKILL.md` inside it. 3. The `SKILL.md` opens with a short YAML-ish header (name, description, version) and then the prompt content. 4. Open a PR — the new directory and its `SKILL.md` are tracked once committed. ## References - [`AGENTS.md`](../AGENTS.md) — this repo's primary agent-context file (open standard). - [Claude Code skills documentation](https://docs.claude.com/en/docs/claude-code/skills) — what a `SKILL.md` looks like and how Claude Code resolves them. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9385612 pynetbox-7.8.0/.claude/skills/0000755000175100017510000000000015215017112015653 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9428709 pynetbox-7.8.0/.claude/skills/run-tests/0000755000175100017510000000000015215017112017617 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.claude/skills/run-tests/SKILL.md0000644000175100017510000000711015215017101021014 0ustar00runnerrunner--- name: run-tests description: Run the pynetbox test suite — unit tests only, integration tests against a live NetBox in Docker, or both. Covers how to pick the right command, pass options, and interpret failures. version: 1 --- # run-tests Run the pynetbox test suite. Choose the scope that fits your situation. ## Quick reference | Goal | Command | |---|---| | Unit tests only (no Docker) | `pytest tests/unit` | | Older-style unit tests only | `pytest tests/test_*.py` | | All unit tests | `pytest tests/unit tests/test_*.py` | | Integration tests — default NetBox version (4.4) | `pytest tests/integration --netbox-versions 4.4` | | Integration tests — specific version | `pytest tests/integration --netbox-versions 4.5` | | Integration tests — multiple versions | `pytest --netbox-versions 4.3,4.4,4.5` | | Against an existing NetBox instance | `pytest tests/integration -p no:docker --url-override http://localhost:8000` | | Leave Docker containers running after tests | `pytest tests/integration --netbox-versions 4.5 --no-cleanup` | | Lint only | `ruff check pynetbox/ tests/` | | Lint + auto-fix | `ruff check --fix pynetbox/ tests/` | ## Before running Activate the virtualenv and confirm dependencies are installed: ```bash source venv/bin/activate pip install -r requirements.txt -r requirements-dev.txt -e . ``` Integration tests require Docker. Confirm the daemon is running (`docker ps`) before invoking them. ## Unit tests Unit tests mock all HTTP responses using JSON fixtures in `tests/fixtures/`. No network access or Docker is needed. ```bash pytest tests/unit tests/test_*.py ``` Useful flags: - `-x` — stop on first failure - `-k ` — run only tests whose name matches the pattern - `-v` — verbose output ## Integration tests Integration tests spin up a `netbox-docker` container, wait for NetBox to be ready, run end-to-end CRUD operations, and tear down the container on exit. ```bash pytest tests/integration --netbox-versions 4.5 ``` - `--netbox-versions` accepts a comma-separated list (`4.3,4.4,4.5`). - Default version when the flag is omitted: **4.4**. - `--no-cleanup` leaves the container running — useful for inspecting state after a failure. - `-p no:docker --url-override http://localhost:8000` skips Docker and targets an existing instance. ## Lint `ruff` is the project linter. CI enforces `ruff check pynetbox/ tests/` on every push. ```bash ruff check pynetbox/ tests/ # check only ruff check --fix pynetbox/ tests/ # auto-fix safe issues ``` Always run the linter before committing; CI will reject lint failures. ## CI matrix The GitHub Actions workflow (`py3.yml`) runs the full matrix: - Python versions: 3.12, 3.13, 3.14 - NetBox versions: 4.3, 4.4, 4.5 Reproducing a specific cell locally: ```bash # Example: Python 3.12 + NetBox 4.3 python3.12 -m pytest tests/ --netbox-versions 4.3 ``` ## Common failure modes | Symptom | Likely cause | Fix | |---|---|---| | Integration tests hang waiting for Docker | Docker not running or image pull stalled | `docker ps`; check daemon logs | | `connection refused` on integration tests | NetBox health check timed out before ready | Re-run; use `--no-cleanup` and inspect container logs | | `ParameterValidationError` in unit tests | Filter param name wrong or fixture out of date | Check the fixture JSON and the parameter name | | Stale `.pyc` / import errors | Bytecode cache out of sync after refactor | `find . -name '*.pyc' -delete` or recreate the venv | | Lint failure on CI but not locally | Different `ruff` version | Pin matches `requirements-dev.txt`; run `pip install -r requirements-dev.txt` | ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1781800521.943034 pynetbox-7.8.0/.github/0000755000175100017510000000000015215017112014377 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9440196 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/0000755000175100017510000000000015215017112016562 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/bug_report.yaml0000644000175100017510000000427715215017101021626 0ustar00runnerrunner--- name: 🐛 Bug Report description: Report a reproducible bug in the current release of pynetbox labels: ["app: pynetbox", "type: bug", "status: needs triage"] body: - type: markdown attributes: value: > **NOTE:** This form is only for reporting _reproducible bugs_ in a current pynetbox release. - type: input attributes: label: pynetbox version description: What version of pynetbox are you currently running? placeholder: v7.8.0 validations: required: true - type: input attributes: label: NetBox version description: What version of NetBox are you currently running? placeholder: v4.3.1 validations: required: true - type: dropdown attributes: label: Python version description: What version of Python are you currently running? options: - "3.10" - "3.11" - "3.12" validations: required: true - type: textarea attributes: label: Steps to Reproduce description: > Please provide a minimal working example to demonstrate the bug. Begin with the initialization of any necessary database objects and clearly enumerate each operation carried out. Ensure that your example is as concise as possible while adequately illustrating the issue. For example: ```python >>> import pynetbox >>> nb = pynebox.api('https://netbox.example.com', token='my-token') ``` Note: **do not utilize the demo instance** for replicating suspected bugs, as its data is subject to change or removal at any time. _Please refrain from including any confidential or sensitive information in your example._ validations: required: true - type: textarea attributes: label: Expected Behavior description: What did you expect to happen? placeholder: The script should execute without raising any errors or exceptions validations: required: true - type: textarea attributes: label: Observed Behavior description: What happened instead? placeholder: A TypeError exception was raised validations: required: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/config.yml0000644000175100017510000000137115215017101020552 0ustar00runnerrunner# Reference: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: false contact_links: - name: 📖 Contributing Policy url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md about: "Please read through our contributing policy before opening an issue or pull request." - name: ❓ Discussion url: https://github.com/netbox-community/pynetbox/discussions about: "If you're just looking for help, try starting a discussion instead." - name: 💬 Community Slack url: https://netdev.chat about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/deprecation.yaml0000644000175100017510000000127315215017101021744 0ustar00runnerrunner--- name: 🗑️ Deprecation description: The removal of an existing feature or resource labels: ["app: pynetbox", "type: deprecation"] body: - type: textarea attributes: label: Proposed Changes description: > Describe in detail the proposed changes. What is being removed? validations: required: true - type: textarea attributes: label: Justification description: Please provide justification for the proposed change(s). validations: required: true - type: textarea attributes: label: Impact description: List all areas of the application that will be affected by this change. validations: required: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/documentation_change.yaml0000644000175100017510000000164415215017101023627 0ustar00runnerrunner--- name: 📖 Documentation Change description: Suggest an addition or modification to the pynetbox documentation labels: ["app: pynetbox", "type: documentation", "status: needs triage"] body: - type: dropdown attributes: label: Change Type description: What type of change are you proposing? options: - Addition - Correction - Removal - Cleanup (formatting, typos, etc.) validations: required: true - type: dropdown attributes: label: Area description: To what section of the documentation does this change primarily pertain? options: - Endpoint - Response - Request - IPAM - Other validations: required: true - type: textarea attributes: label: Proposed Changes description: Describe the proposed changes and why they are necessary. validations: required: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/feature_request.yaml0000644000175100017510000000430715215017101022653 0ustar00runnerrunner--- name: ✨ Feature Request description: Propose a new pynetbox feature or enhancement labels: ["app: pynetbox", "type: feature", "status: needs triage"] body: - type: markdown attributes: value: > **NOTE:** This form is only for submitting well-formed proposals to extend or modify pynetbox in some way. If you're trying to solve a problem but can't figure out how, or if you still need time to work on the details of a proposed new feature, please start a [discussion](https://github.com/netbox-community/pynetbox/discussions) instead. - type: input attributes: label: pynetbox version description: What version of pynetbox are you currently running? placeholder: v7.8.0 validations: required: true - type: input attributes: label: NetBox version description: What version of NetBox are you currently running? placeholder: v3.6.0 validations: required: true - type: dropdown attributes: label: Feature type options: - Data model extension - New functionality - Change to existing functionality validations: required: true - type: textarea attributes: label: Proposed functionality description: > Describe in detail the new feature or behavior you are proposing. Include any specific changes to work flows, data models, and/or the user interface. The more detail you provide here, the greater chance your proposal has of being discussed. Feature requests which don't include an actionable implementation plan will be rejected. validations: required: true - type: textarea attributes: label: Use case description: > Explain how adding this functionality would benefit pynetbox users. What need does it address? validations: required: true - type: textarea attributes: label: External dependencies description: > List any new dependencies on external libraries or services that this new feature would introduce. For example, does the proposal require the installation of a new Python package? (Not all new features introduce new dependencies.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/ISSUE_TEMPLATE/housekeeping.yaml0000644000175100017510000000151415215017101022133 0ustar00runnerrunner--- name: 🏡 Housekeeping description: A change pertaining to the codebase itself (developers only) labels: ["app: pynetbox", "type: housekeeping"] body: - type: markdown attributes: value: > **NOTE:** This template is for use by maintainers only. Please do not submit an issue using this template unless you have been specifically asked to do so. - type: textarea attributes: label: Proposed Changes description: > Describe in detail the new feature or behavior you'd like to propose. Include any specific changes to work flows, data models, or the user interface. validations: required: true - type: textarea attributes: label: Justification description: Please provide justification for the proposed change(s). validations: required: true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/PULL_REQUEST_TEMPLATE.md0000644000175100017510000000113415215017101020175 0ustar00runnerrunner ### Fixes: #1234 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9445658 pynetbox-7.8.0/.github/workflows/0000755000175100017510000000000015215017112016434 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/workflows/build-mkdocs.yml0000644000175100017510000000131615215017101021533 0ustar00runnerrunnername: Build MkDocs on: push: branches: - master - main permissions: contents: write jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout pynetbox uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.x - name: Install dependencies run: pip install mkdocs-material mkdocs-autorefs mkdocs-material-extensions mkdocstrings mkdocstrings-python-legacy mkdocs-include-markdown-plugin pymdown-extensions markdown-include - name: Deploy docs run: mkdocs gh-deploy --force ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/workflows/claude.yml0000644000175100017510000000252015215017101020411 0ustar00runnerrunnername: Claude Code on: issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request_review: types: [submitted] concurrency: group: claude-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: true jobs: claude: if: | (github.event_name != 'issue_comment' || github.event.issue.pull_request != null) && contains(github.event.comment.body || github.event.review.body, '@claude') && (github.event.comment.user.type || github.event.review.user.type) != 'Bot' && contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || github.event.review.author_association) runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read issues: write pull-requests: write actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - name: Run Claude Code id: claude uses: anthropics/claude-code-action@11a9dadd198803a0cea6bd53da3e0e8a762fc6ea # v1.0.108 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/workflows/no-blank-issue.yml0000644000175100017510000000062215215017101022004 0ustar00runnerrunnername: Enforce issue templates on: issues: types: - opened - reopened permissions: issues: write jobs: no-blank-issue: name: No Blank Issue runs-on: ubuntu-slim steps: - name: Close new issues without labels uses: ldez/no-blank-issue@800e2d0c81c9e0ca7bdb58f3e7480a74602d91e0 # v1.2.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/workflows/publish.yml0000644000175100017510000000234415215017101020626 0ustar00runnerrunner# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout pynetbox uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.github/workflows/py3.yml0000644000175100017510000000211015215017101017662 0ustar00runnerrunnername: Py3 Test on: pull_request: {} push: branches: - master jobs: build: runs-on: ubuntu-latest strategy: matrix: python: ["3.12", "3.13", "3.14"] netbox: ["4.4", "4.5", "4.6"] steps: - name: Checkout pynetbox uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python }} - name: Install dev requirements run: pip install -r requirements-dev.txt . - name: Enable Docker IPv6 run: | echo '{"ipv6": true, "fixed-cidr-v6": "fd00::/80"}' | sudo tee /etc/docker/daemon.json sudo systemctl reload docker - name: Free up Docker resources run: | docker system prune -af --volumes docker network prune -f - name: Run Linter run: ruff check pynetbox/ tests/ - name: Run Tests run: pytest --netbox-versions=${{ matrix.netbox }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.gitignore0000644000175100017510000000243515215017101015031 0ustar00runnerrunner# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ lib/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # AI tooling .claude/settings.local.json # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # Other git repos checked out locally .netbox-docker-*/ .devicetype-library/ # Visual Studio Code settings .vscode/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.pre-commit-config.yaml0000644000175100017510000000042415215017101017316 0ustar00runnerrunner# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.14.1 hooks: - id: ruff name: "Ruff linter" args: [ pynetbox/, tests/ ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/.readthedocs.yaml0000644000175100017510000000042015215017101016260 0ustar00runnerrunnerversion: 2 build: os: ubuntu-22.04 tools: python: "3.11" # Build from the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Explicitly set the version of Python and its requirements python: install: - requirements: docs/requirements.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/AGENTS.md0000644000175100017510000004001715215017101014342 0ustar00runnerrunner# AGENTS.md — pynetbox ## Repository Overview `pynetbox` is a Python API client library for NetBox. It provides a Pythonic interface to interact with NetBox's REST API and supports NetBox 3.3+ (pynetbox 6.7+). It is a standalone library — not a Django app or NetBox plugin — published to PyPI and used by automation tooling, scripts, and other Python projects to read and write NetBox data. Current version: `7.7.0` (see `pynetbox/__init__.py`). ## Tech Stack - Python 3.10+ (tested on 3.12, 3.13, 3.14 in CI) - `requests` + `urllib3` for HTTP (pins in `requirements.txt`) - `packaging` for version comparisons - `pytest` + `pytest-docker` for the test suite (unit and integration) - `ruff` for linting (no `ruff.toml`; configured inline or with defaults) - `mkdocs-material` + `mkdocstrings` for user-facing docs Defer all version pins to `requirements.txt` and `requirements-dev.txt`. Package metadata lives in `setup.py` (uses `setuptools_scm` for version from git tags). ## Repository Map ```text . ├── pynetbox/ │ ├── __init__.py — Public API: Api, exceptions, __version__. │ ├── core/ │ │ ├── api.py — Api class: entry point, session management, app init. │ │ ├── app.py — App and PluginsApp: dynamic endpoint access. │ │ ├── endpoint.py — Endpoint: CRUD operations, filter validation. │ │ ├── query.py — Request: HTTP layer, threading, exceptions. │ │ ├── response.py — Record, RecordSet: deserialization, save/delete. │ │ └── util.py — Shared utilities. │ └── models/ │ ├── circuits.py — Circuit-specific Record subclasses. │ ├── core.py — DataSources, Jobs, ObjectChanges models. │ ├── dcim.py — DCIM models: Devices, Interfaces, Cables, tracing. │ ├── extras.py — Custom fields, tags, webhooks. │ ├── ipam.py — IP address management: IpAddresses, Prefixes, VLANs. │ ├── mapper.py — CONTENT_TYPE_MAPPER: content-type string → Record class. │ ├── users.py — User and permission models. │ ├── virtualization.py — Virtual machine models. │ └── wireless.py — Wireless endpoint models. ├── tests/ │ ├── conftest.py — Pytest options: --netbox-versions, --no-cleanup, --url-override. │ ├── fixtures/ — JSON response fixtures for unit tests (organised by app). │ ├── test_api.py — Api-level unit tests. │ ├── test_app.py — App/PluginsApp unit tests. │ ├── test_circuits.py — Circuits unit tests (older style). │ ├── test_dcim.py — DCIM unit tests (older style). │ ├── test_tenancy.py — Tenancy unit tests. │ ├── test_users.py — Users unit tests. │ ├── test_virtualization.py — Virtualization unit tests. │ ├── test_wireless.py — Wireless unit tests. │ ├── util.py — Shared test helpers. │ ├── unit/ — Newer unit tests (mock HTTP responses). │ │ ├── test_detailendpoint.py │ │ ├── test_endpoint.py │ │ ├── test_endpoint_strict_filter.py │ │ ├── test_extras.py │ │ ├── test_file_upload.py │ │ ├── test_mapper.py │ │ ├── test_multiformat_endpoint.py │ │ ├── test_query.py │ │ ├── test_request.py │ │ └── test_response.py │ └── integration/ — Integration tests against live NetBox in Docker. │ ├── conftest.py — Docker setup: spin up netbox-docker, wait for ready. │ ├── test_circuits.py │ ├── test_dcim.py │ └── test_ipam.py ├── docs/ — mkdocs site (API reference + guides). ├── .github/workflows/ │ ├── py3.yml — Lint + tests on every push/PR (matrix: Python × NetBox). │ ├── publish.yml — PyPI publish on GitHub release. │ └── build-mkdocs.yml — Deploy docs to GitHub Pages on push to master. ├── AGENTS.md — This file. ├── CLAUDE.md — Shim that pulls in this file. ├── CHANGELOG.md — Release history. ├── mkdocs.yml — Docs site config. ├── requirements.txt — Runtime dependencies. ├── requirements-dev.txt — Dev/test dependencies. └── setup.py — Package metadata; version from setuptools_scm. ``` ## Architecture The library follows a layered architecture. Each layer wraps the one below it. ### API Layer (`pynetbox/core/api.py`) `Api` is the main entry point. On construction it eagerly creates `App` instances for every built-in NetBox application (dcim, ipam, circuits, virtualization, extras, users, wireless, core, vpn, tenancy) plus a `PluginsApp` for plugin endpoints. It owns the `requests.Session`, authentication token, threading flag, and `strict_filters` setting. ### App Layer (`pynetbox/core/app.py`) `App` represents a NetBox application. Attribute access (`nb.dcim.devices`) is handled by `__getattr__`, which lazily constructs and returns an `Endpoint`. `PluginsApp` adds plugin namespace handling, converting dashes to underscores so `nb.plugins.my_plugin.objects` works. ### Endpoint Layer (`pynetbox/core/endpoint.py`) `Endpoint` exposes CRUD operations: | Method | Description | |---|---| | `.all()` | Return all objects (paginated, threading-aware). | | `.filter(**kwargs)` | Return matching objects. | | `.get(id_or_kwargs)` | Return a single object. | | `.count(**kwargs)` | Return a count. | | `.create(data)` | Create one or more objects. | | `.update(data)` | Bulk-update objects. | | `.delete(data)` | Bulk-delete objects. | | `.choices()` | Return field choices from the OPTIONS response. | Endpoint names are translated from Python attribute style to URL slugs (`ip_addresses` → `ip-addresses`). When `strict_filters=True`, filter kwargs are validated against the OpenAPI spec before the request is sent. ### Query Layer (`pynetbox/core/query.py`) `Request` handles all HTTP communication. It supports threading for `.all()` and `.filter()` operations (parallel page fetching). Custom exceptions: `RequestError`, `AllocationError`, `ContentError`, `ParameterValidationError`. ### Response Layer (`pynetbox/core/response.py`) `Record` represents a single API object. Supports attribute access, dict-like access, `.save()` (PATCH), and `.delete()`. `RecordSet` is a lazy iterable returned by `.all()` and `.filter()`; it pages through results on demand. ### Models Module (`pynetbox/models/`) Custom `Record` subclasses in `pynetbox/models/` add endpoint-specific behaviour: | File | Content | |---|---| | `dcim.py` | `TraceableRecord` (cable tracing), `Cables` (A/B termination serialization), `DetailEndpoints`. | | `ipam.py` | `IpAddresses`, `Prefixes`, `VLANs` and related types. | | `circuits.py` | Circuit-specific record types. | | `virtualization.py` | Virtual machine record types. | | `extras.py` | Custom fields, tags, webhooks. | | `users.py` | User and permission record types. | | `wireless.py` | Wireless endpoint record types. | | `core.py` | `DataSources`, `Jobs`, `ObjectChanges`. | | `mapper.py` | `CONTENT_TYPE_MAPPER`: maps NetBox content-type strings (e.g. `"dcim.device"`) to their custom `Record` subclass; used when resolving polymorphic nested objects. | ### Key Design Patterns - **Lazy endpoint creation**: `Endpoint` objects are constructed on first attribute access via `App.__getattr__`; top-level `App` objects are created eagerly in `Api.__init__`. - **Threading**: Enable with `threading=True` in `Api()` for parallel page fetching on `.all()` / `.filter()`. - **Custom sessions**: Replace `api.http_session` with a configured `requests.Session` for custom SSL certs, timeouts, or retry logic. - **Branch support**: `api.activate_branch(branch_name)` context manager for the NetBox branching plugin. - **Strict filter validation**: `strict_filters=True` validates filter parameters against the OpenAPI spec before sending requests, raising `ParameterValidationError` for unknown params. ## Commands | Command | What it does | |---|---| | `pip install -r requirements.txt && pip install -r requirements-dev.txt && pip install -e .` | Install all dependencies in editable mode | | `pytest tests/unit` | Run unit tests only (no Docker required) | | `pytest tests/integration --netbox-versions 4.5` | Run integration tests against NetBox 4.5 (requires Docker) | | `pytest --netbox-versions 4.3,4.4,4.5` | Run against multiple NetBox versions | | `pytest --no-cleanup` | Leave Docker containers running after tests | | `pytest -p no:docker --url-override http://localhost:8000` | Run integration tests against an existing NetBox instance | | `ruff check pynetbox/ tests/` | Run linter | | `ruff check --fix pynetbox/ tests/` | Fix auto-fixable lint issues | | `mkdocs serve` | Preview docs locally | | `mkdocs gh-deploy` | Deploy docs to GitHub Pages | | `python -m build` | Build sdist + wheel (matches the release workflow) | ## Development ```bash python3 -m venv venv source venv/bin/activate pip install -r requirements.txt -r requirements-dev.txt -e . ``` Integration tests require Docker. The `tests/integration/conftest.py` uses `pytest-docker` to pull and start `netbox-docker` containers, wait for NetBox to be ready, run the tests, and tear down (unless `--no-cleanup`). ## Testing - **Unit tests** (`tests/unit/` and `tests/test_*.py`) mock HTTP responses with JSON fixtures from `tests/fixtures/`. No Docker required. - **Integration tests** (`tests/integration/`) run against a real NetBox instance in Docker. They test end-to-end CRUD operations across dcim, ipam, and circuits. - `tests/conftest.py` defines three pytest options: `--netbox-versions` (comma-separated, default `4.4`), `--no-cleanup`, and `--url-override`. | Test module | Coverage area | |---|---| | `tests/unit/test_endpoint.py` | Endpoint CRUD method behaviour | | `tests/unit/test_endpoint_strict_filter.py` | Strict filter parameter validation | | `tests/unit/test_detailendpoint.py` | DetailEndpoint (objects without list views) | | `tests/unit/test_response.py` | Record / RecordSet deserialization | | `tests/unit/test_request.py` | Request HTTP layer | | `tests/unit/test_query.py` | Query construction | | `tests/unit/test_extras.py` | Extras model handling | | `tests/unit/test_mapper.py` | CONTENT_TYPE_MAPPER resolution | | `tests/unit/test_file_upload.py` | File upload support | | `tests/unit/test_multiformat_endpoint.py` | Multi-format endpoint responses | | `tests/test_api.py` | Api class initialization and status | | `tests/test_app.py` | App / PluginsApp attribute access | | `tests/test_dcim.py` | DCIM model behavior (older style) | | `tests/test_circuits.py` | Circuits model behavior (older style) | | `tests/test_tenancy.py` | Tenancy model behavior | | `tests/test_users.py` | Users model behavior | | `tests/test_virtualization.py` | Virtualization model behavior | | `tests/test_wireless.py` | Wireless model behavior | | `tests/integration/test_dcim.py` | DCIM end-to-end against live NetBox | | `tests/integration/test_ipam.py` | IPAM end-to-end against live NetBox | | `tests/integration/test_circuits.py` | Circuits end-to-end against live NetBox | ## CI/CD GitHub Actions workflows in `.github/workflows/`: - **`py3.yml`** — Runs on every push/PR. Matrix: Python × {3.12, 3.13, 3.14} and NetBox × {4.3, 4.4, 4.5}. Enables Docker IPv6, runs `ruff check`, then `pytest --netbox-versions=${{ matrix.netbox }}` (integration + unit). - **`publish.yml`** — Runs on published GitHub releases. Builds sdist + wheel with `python -m build`, then publishes to PyPI using a token secret (`PYPI_API_TOKEN`). - **`build-mkdocs.yml`** — Runs on push to `master`/`main`. Deploys docs to GitHub Pages with `mkdocs gh-deploy --force`. ## Common Tasks ### Add support for a new NetBox endpoint 1. If the endpoint returns objects that need custom behaviour (e.g. nested serialization, tracing), add a `Record` subclass to the appropriate `pynetbox/models/.py`. 2. Register the subclass in `CONTENT_TYPE_MAPPER` in `pynetbox/models/mapper.py` if it can appear as a nested polymorphic object. 3. Add unit tests in `tests/unit/` with fixture JSON in `tests/fixtures//`. 4. Add integration tests in `tests/integration/test_.py` if appropriate. ### Add or change serialization behavior Serialization for PATCH/PUT/POST is handled in `Record.serialize()` (`pynetbox/core/response.py`). Nested objects are reduced to IDs; tags and certain list fields are treated as sets. Custom serialization for specific models lives in their `Record` subclass. ### Bump the supported NetBox version 1. Update `DEFAULT_NETBOX_VERSIONS` in `tests/conftest.py` if changing the default test target. 2. Widen the `netbox` matrix in `.github/workflows/py3.yml`. 3. Update the version compatibility table in this file and in `CHANGELOG.md`. 4. Run integration tests locally against the new version. ### Cut a release 1. Ensure all changes are merged to `master`. 2. Tag the commit with the new version (e.g. `git tag v7.8.0`). `setuptools_scm` derives `__version__` from the tag. 3. Publish a GitHub release. `publish.yml` builds and pushes to PyPI automatically. 4. Update `CHANGELOG.md`. ## Conventions and Patterns - **No Django, no ORM.** pynetbox is a pure Python HTTP client; do not introduce Django or ORM dependencies. - **`Record` subclasses for model-specific behavior.** Specialised deserialization, computed properties, and serialization overrides belong in `pynetbox/models/.py`, not in the core layer. - **`CONTENT_TYPE_MAPPER` for polymorphic resolution.** When a nested object's type is determined at runtime (e.g. a cable termination), `mapper.py` maps the NetBox content-type string to the correct `Record` subclass. - **Underscores to dashes in URL slugs.** `Endpoint.__init__` converts `ip_addresses` → `ip-addresses` automatically; do not hardcode slugs elsewhere. - **Threading is opt-in.** The default `Api()` is single-threaded. Threading is enabled per-instance with `threading=True`; thread safety is the caller's responsibility beyond that. - **Commit messages** should be prefixed with `Fixes #NNN:` or `Closes #NNN:` referencing the GitHub issue. - **Linting**: `ruff check pynetbox/ tests/` must pass before committing. ## Troubleshooting - **`RequestError` on every call** — Verify the `url` passed to `Api()` includes the scheme (`http://` or `https://`) and no trailing slash issues. Check that the token is valid. - **`ParameterValidationError` for a valid filter** — Either the filter name is wrong (check the NetBox API docs) or the OpenAPI spec for your NetBox version doesn't list it. Use `strict_filters=False` (the default) to bypass validation, or fix the parameter name. - **Integration tests hang waiting for Docker** — Docker may not be running or the `netbox-docker` image pull is stalled. Check `docker ps` and Docker daemon logs. - **Integration tests fail with connection refused** — NetBox health check may have timed out before the instance was ready. Re-run with `--no-cleanup` and inspect the container logs. - **Stale `.pyc` / import errors after refactoring** — Run `find . -name '*.pyc' -delete` or recreate the venv. ## Version Compatibility NetBox version compatibility is strict. Defer to `CHANGELOG.md` for the full history. | pynetbox | NetBox | |---|---| | 7.7.0 | 4.3, 4.4, 4.5 | | 7.6.1 | 4.5.0 | | 7.5.0 | 4.1, 4.2, 4.3 | | 7.4.1 | 4.0.6 | | 7.0.0+ | 3.3+ | ## References - GitHub repository: - PyPI package: - User docs (mkdocs): [`docs/`](./docs/) - NetBox REST API docs: - Release history: [`CHANGELOG.md`](./CHANGELOG.md) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/CHANGELOG.md0000644000175100017510000000012715215017101014646 0ustar00runnerrunner For the list of changelog, please see the repository releases information in GitHub. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/CLAUDE.md0000644000175100017510000000001515215017101014310 0ustar00runnerrunner@./AGENTS.md ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/LICENSE0000644000175100017510000002261415215017101014047 0ustar00runnerrunner Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2017 DigitalOcean././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9693332 pynetbox-7.8.0/PKG-INFO0000644000175100017510000001225615215017112014142 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pynetbox Version: 7.8.0 Summary: NetBox API client library Home-page: https://github.com/netbox-community/pynetbox Author: Zach Moody, Arthur Hanson Author-email: ahanson@netboxlabs.com License: Apache2 Keywords: netbox Classifier: Intended Audience :: Developers Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: requests<3.0,>=2.20.0 Requires-Dist: packaging Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: requires-dist Dynamic: summary # Pynetbox Python API client library for [NetBox](https://github.com/netbox-community/netbox). > **Note:** Version 6.7 and later of the library only supports NetBox 3.3 and above. ## Compatibility Each pyNetBox Version listed below has been tested with its corresponding NetBox Version. | NetBox Version | Plugin Version | |:--------------:|:--------------:| | 4.6 | 7.8.0 | | 4.6 | 7.7.0 | | 4.5 | 7.6.1 | | 4.5 | 7.6.0 | | 4.4 | 7.5.0 | | 4.3 | 7.5.0 | | 4.2 | 7.5.0 | | 4.1 | 7.5.0 | | 4.0.6 | 7.4.1 | | 4.0.0 | 7.3.4 | | 3.7 | 7.3.0 | | 3.6 | 7.2.0 | | 3.5 | 7.1.0 | | 3.3 | 7.0.0 | ## Installation To install run `pip install pynetbox`. Alternatively, you can clone the repo and run `python setup.py install`. ## Quick Start The full pynetbox API is documented on [GitHub Pages](https://netbox-community.github.io/pynetbox/), but the following should be enough to get started using it. To begin, import pynetbox and instantiate the API. ``` import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) ``` The first argument the .api() method takes is the NetBox URL. There are a handful of named arguments you can provide, but in most cases none are required to simply pull data. In order to write, the `token` argument should to be provided. ## Queries The pynetbox API is setup so that NetBox's apps are attributes of the `.api()` object, and in turn those apps have attribute representing each endpoint. Each endpoint has a handful of methods available to carry out actions on the endpoint. For example, in order to query all the objects in the `devices` endpoint you would do the following: ``` >>> devices = nb.dcim.devices.all() >>> for device in devices: ... print(device.name) ... test1-leaf1 test1-leaf2 test1-leaf3 >>> ``` Note that the all() and filter() methods are generators and return an object that can be iterated over only once. If you are going to be iterating over it repeatedly you need to either call the all() method again, or encapsulate the results in a `list` object like this: ``` >>> devices = list(nb.dcim.devices.all()) ``` ### Threading pynetbox supports multithreaded calls for `.filter()` and `.all()` queries. It is **highly recommended** you have `MAX_PAGE_SIZE` in your Netbox install set to anything *except* `0` or `None`. The default value of `1000` is usually a good value to use. To enable threading, add `threading=True` parameter to the `.api`: ```python nb = pynetbox.api( 'http://localhost:8000', threading=True, ) ``` ### Filters validation NetBox doesn't validate filters passed to the GET API endpoints, which are accessed with `.get()` and `.filter()`. If a filter is incorrect, NetBox silently returns the entire database table content. Pynetbox allows to check provided parameters against NetBox OpenAPI specification before doing the call, and raise an exception if a parameter is incorrect. This can be enabled globally by setting `strict_filters=True` in the API object initialization: ```python nb = pynetbox.api( 'http://localhost:8000', strict_filters=True, ) ``` This can also be enabled and disabled on a per-request basis: ```python # Disable for one request when enabled globally. # Will not raise an exception and return the entire Device table. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=False) # Enable for one request when not enabled globally. # Will raise an exception. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=True) ``` ## Running Tests First, create and activate a Python virtual environment in the pynetbox directory to isolate the project dependencies: ```python python3 -m venv venv source venv/bin/activate ``` Install both requirements files: ```python pip install -r requirements.txt pip install -r requirements-dev.txt ``` The test suite requires Docker to be installed and running, as it will download and launch netbox-docker containers during test execution. With Docker installed and running, execute the following command to run the test suite: ```python pytest ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/README.md0000644000175100017510000001044715215017101014322 0ustar00runnerrunner# Pynetbox Python API client library for [NetBox](https://github.com/netbox-community/netbox). > **Note:** Version 6.7 and later of the library only supports NetBox 3.3 and above. ## Compatibility Each pyNetBox Version listed below has been tested with its corresponding NetBox Version. | NetBox Version | Plugin Version | |:--------------:|:--------------:| | 4.6 | 7.8.0 | | 4.6 | 7.7.0 | | 4.5 | 7.6.1 | | 4.5 | 7.6.0 | | 4.4 | 7.5.0 | | 4.3 | 7.5.0 | | 4.2 | 7.5.0 | | 4.1 | 7.5.0 | | 4.0.6 | 7.4.1 | | 4.0.0 | 7.3.4 | | 3.7 | 7.3.0 | | 3.6 | 7.2.0 | | 3.5 | 7.1.0 | | 3.3 | 7.0.0 | ## Installation To install run `pip install pynetbox`. Alternatively, you can clone the repo and run `python setup.py install`. ## Quick Start The full pynetbox API is documented on [GitHub Pages](https://netbox-community.github.io/pynetbox/), but the following should be enough to get started using it. To begin, import pynetbox and instantiate the API. ``` import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) ``` The first argument the .api() method takes is the NetBox URL. There are a handful of named arguments you can provide, but in most cases none are required to simply pull data. In order to write, the `token` argument should to be provided. ## Queries The pynetbox API is setup so that NetBox's apps are attributes of the `.api()` object, and in turn those apps have attribute representing each endpoint. Each endpoint has a handful of methods available to carry out actions on the endpoint. For example, in order to query all the objects in the `devices` endpoint you would do the following: ``` >>> devices = nb.dcim.devices.all() >>> for device in devices: ... print(device.name) ... test1-leaf1 test1-leaf2 test1-leaf3 >>> ``` Note that the all() and filter() methods are generators and return an object that can be iterated over only once. If you are going to be iterating over it repeatedly you need to either call the all() method again, or encapsulate the results in a `list` object like this: ``` >>> devices = list(nb.dcim.devices.all()) ``` ### Threading pynetbox supports multithreaded calls for `.filter()` and `.all()` queries. It is **highly recommended** you have `MAX_PAGE_SIZE` in your Netbox install set to anything *except* `0` or `None`. The default value of `1000` is usually a good value to use. To enable threading, add `threading=True` parameter to the `.api`: ```python nb = pynetbox.api( 'http://localhost:8000', threading=True, ) ``` ### Filters validation NetBox doesn't validate filters passed to the GET API endpoints, which are accessed with `.get()` and `.filter()`. If a filter is incorrect, NetBox silently returns the entire database table content. Pynetbox allows to check provided parameters against NetBox OpenAPI specification before doing the call, and raise an exception if a parameter is incorrect. This can be enabled globally by setting `strict_filters=True` in the API object initialization: ```python nb = pynetbox.api( 'http://localhost:8000', strict_filters=True, ) ``` This can also be enabled and disabled on a per-request basis: ```python # Disable for one request when enabled globally. # Will not raise an exception and return the entire Device table. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=False) # Enable for one request when not enabled globally. # Will raise an exception. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=True) ``` ## Running Tests First, create and activate a Python virtual environment in the pynetbox directory to isolate the project dependencies: ```python python3 -m venv venv source venv/bin/activate ``` Install both requirements files: ```python pip install -r requirements.txt pip install -r requirements-dev.txt ``` The test suite requires Docker to be installed and running, as it will download and launch netbox-docker containers during test execution. With Docker installed and running, execute the following command to run the test suite: ```python pytest ``` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1781800521.946478 pynetbox-7.8.0/docs/0000755000175100017510000000000015215017112013767 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/advanced.md0000644000175100017510000002766615215017101016075 0ustar00runnerrunner# Advanced Usage ## Threading pynetbox supports multithreaded page fetching for `.filter()` and `.all()` queries, which can significantly improve performance when iterating large result sets. !!! warning "NetBox Configuration Required" For threading to be effective, `MAX_PAGE_SIZE` in your NetBox installation must be set to a finite value (not `0` or `None`). The default of `1000` is usually a good choice. ### Enabling Threading Enable threading globally by passing `threading=True` when constructing the API client: ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-token', threading=True, ) # .all() and .filter() now fetch pages in parallel devices = nb.dcim.devices.all() ``` ### How It Works When threading is enabled, pynetbox issues an initial request to determine the total record count, then dispatches concurrent requests for the remaining pages. The records are streamed back through the same `RecordSet` interface as a single-threaded query. Threading is opt-in per API client; thread safety beyond pynetbox's own page-fetching is the caller's responsibility. ### Example ```python import pynetbox import time # Single-threaded nb = pynetbox.api('http://localhost:8000', token='your-token') start = time.time() devices = list(nb.dcim.devices.all()) print(f"Without threading: {time.time() - start:.2f}s") # Multithreaded nb_threaded = pynetbox.api( 'http://localhost:8000', token='your-token', threading=True, ) start = time.time() devices = list(nb_threaded.dcim.devices.all()) print(f"With threading: {time.time() - start:.2f}s") ``` ### Tuning the Worker Count By default, threaded queries use up to 4 worker threads. Override this with `max_workers`: ```python nb = pynetbox.api( 'http://localhost:8000', token='your-token', threading=True, max_workers=8, ) ``` ### Custom Thread Pool Executor By default pynetbox builds its pool with `concurrent.futures.ThreadPoolExecutor`. You can inject your own executor with `thread_pool_executor`. It must be a callable matching the `ThreadPoolExecutor(max_workers=...)` signature and support the context-manager protocol — a `ThreadPoolExecutor` subclass is the simplest option. This is useful when worker threads need thread-local state that the standard executor does not propagate, such as an OpenTelemetry trace context, a request-scoped logging correlation ID, or a thread-bound database/session handle. Without propagation, work performed in the pool's threads is detached from the context of the calling thread. ```python import concurrent.futures from opentelemetry import context as otel_context class ContextPropagatingExecutor(concurrent.futures.ThreadPoolExecutor): """Carries the caller's OpenTelemetry context into each worker thread.""" def submit(self, fn, *args, **kwargs): ctx = otel_context.get_current() def run_with_context(): token = otel_context.attach(ctx) try: return fn(*args, **kwargs) finally: otel_context.detach(token) return super().submit(run_with_context) nb = pynetbox.api( 'http://localhost:8000', token='your-token', threading=True, thread_pool_executor=ContextPropagatingExecutor, ) ``` The executor is constructed once per threaded query and shut down when that query's pages have been fetched, so pass the class (or a factory), not an already-instantiated pool. ## Cursor-Based Pagination Starting with NetBox 4.6, the REST API supports cursor-based pagination as an alternative to the default offset-based pagination. Instead of skipping a growing number of rows with an `offset`, the server pages forward using the primary key (`id`) as a cursor. This avoids the cost of scanning the table up to the offset position, so it performs significantly better on very large result sets. ### Enabling Cursor Pagination Pass `pagination="cursor"` when constructing the API client: ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-token', pagination="cursor", ) # .all() and .filter() now page using the server's `start` cursor devices = nb.dcim.devices.all() ``` pynetbox probes the NetBox version once on the first query. If the server is older than 4.6 (and therefore does not support cursor pagination), it transparently falls back to offset-based pagination, so it is safe to leave this option enabled across mixed environments. ### Trade-offs !!! note "Things to know about cursor pagination" - **No total count up front.** NetBox omits the `count` in cursor mode for performance. pynetbox still supports `len(record_set)`, but doing so triggers a separate count request. - **Fixed ordering.** Results are always ordered by `id`; an explicit `ordering` filter cannot be combined with cursor pagination. - **Not combined with threading.** Cursor pagination is inherently sequential (each page's cursor depends on the previous page), so it cannot be parallelized. When `pagination="cursor"` is set, queries page sequentially even if `threading=True`. For parallel fetching of large offset-paginated result sets, use threading instead (see above). - **Explicit `offset` still works.** Passing an explicit `offset` to `.all()`/`.filter()` requests a single offset-based page as before, since `start` and `offset` are mutually exclusive on the server. ## Filter Validation NetBox does not validate filter parameters passed to list endpoints. An unrecognized parameter is silently ignored, which means a typo in a `.filter()` or `.get()` call can quietly return the entire table. pynetbox can optionally validate filter parameters against NetBox's OpenAPI specification before making the request, raising `ParameterValidationError` if any parameter is unrecognized. ### Enabling Strict Filters Globally ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-token', strict_filters=True, ) try: nb.dcim.devices.filter(non_existing_filter='value') except pynetbox.ParameterValidationError as e: print(f"Invalid filter: {e}") ``` ### Per-Request Validation Validation can also be toggled per request by passing `strict_filters` directly to `.filter()` or `.get()`. The per-request value overrides the global setting. ```python nb = pynetbox.api('http://localhost:8000', token='your-token') # Enable for one request (when not globally enabled) try: nb.dcim.devices.filter( non_existing_filter='aaaa', strict_filters=True, ) except pynetbox.ParameterValidationError as e: print(f"Invalid filter: {e}") # Disable for one request (when globally enabled) nb_strict = pynetbox.api( 'http://localhost:8000', token='your-token', strict_filters=True, ) # Skips validation; NetBox will accept the request but ignore the unknown filter nb_strict.dcim.devices.filter( non_existing_filter='aaaa', strict_filters=False, ) ``` !!! note "Custom field filters" Custom field filters (`cf_` and their lookup suffixes such as `cf___gt`) are dynamic and not listed in the OpenAPI specification. They are skipped by the validator and never raise `ParameterValidationError`. ### Benefits - **Catch typos early**: surface misspelled filter names before they cause silent full-table scans. - **Better error messages**: failures point to the exact invalid parameter. - **Safer development**: enable globally during development and disable in production hot paths if needed. ### Example ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-token', strict_filters=True, ) # Valid filter devices = nb.dcim.devices.filter(site='datacenter1') # Invalid filter try: devices = nb.dcim.devices.filter(iste='datacenter1') # typo except pynetbox.ParameterValidationError as e: print(f"Error: {e}") ``` ## Custom Sessions You can substitute pynetbox's default `requests.Session` with your own to customize HTTP behavior such as headers, SSL verification, timeouts, and retries. ### Custom Headers To set custom headers on every request: ```python import pynetbox import requests session = requests.Session() session.headers = {'mycustomheader': 'test'} nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) nb.http_session = session ``` Custom headers are merged with the headers pynetbox sets internally. ### Disabling SSL Verification To disable SSL certificate verification (for self-signed certificates in lab environments). See the [requests docs](https://requests.readthedocs.io/en/stable/user/advanced/#ssl-cert-verification) for additional options: ```python import pynetbox import requests session = requests.Session() session.verify = False nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) nb.http_session = session ``` ### Timeouts Setting a default timeout requires a custom HTTP adapter: ```python import pynetbox import requests from requests.adapters import HTTPAdapter class TimeoutHTTPAdapter(HTTPAdapter): def __init__(self, *args, **kwargs): self.timeout = kwargs.pop('timeout', 5) super().__init__(*args, **kwargs) def send(self, request, **kwargs): kwargs['timeout'] = self.timeout return super().send(request, **kwargs) adapter = TimeoutHTTPAdapter(timeout=10) session = requests.Session() session.mount('http://', adapter) session.mount('https://', adapter) nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) nb.http_session = session ``` ## File Uploads (Image Attachments) pynetbox supports file uploads on endpoints that accept them, such as image attachments. When a file-like object (anything with a callable `.read()`) is passed to `.create()`, pynetbox automatically switches to `multipart/form-data` encoding instead of JSON. ### Creating an Image Attachment ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) with open('/path/to/image.png', 'rb') as f: attachment = nb.extras.image_attachments.create( object_type='dcim.device', object_id=1, image=f, name='rack-photo.png', ) ``` ### Using `io.BytesIO` In-memory file objects work the same way: ```python import io import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) image_data = b'...' # raw image bytes file_obj = io.BytesIO(image_data) file_obj.name = 'generated-image.png' # optional: filename hint attachment = nb.extras.image_attachments.create( object_type='dcim.device', object_id=1, image=file_obj, ) ``` ### Custom Filename and Content-Type For full control over the multipart part, pass a tuple in place of the file object. The accepted forms are `(filename, file_object)` and `(filename, file_object, content_type)`: ```python with open('/path/to/image.png', 'rb') as f: attachment = nb.extras.image_attachments.create( object_type='dcim.device', object_id=1, image=('custom-name.png', f, 'image/png'), ) ``` ## Multi-Format Responses Some endpoints can return data in multiple formats. The rack elevation endpoint, for example, supports both JSON (a list of rack-unit objects) and SVG (a rendered diagram). ### Rack Elevation as JSON By default the elevation endpoint returns JSON: ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f', ) rack = nb.dcim.racks.get(123) # Returns a list of RU objects for unit in rack.elevation.list(): print(unit.id, unit.name) ``` ### Rack Elevation as SVG Pass `render='svg'` to get a rendered SVG diagram as a string: ```python rack = nb.dcim.racks.get(123) svg_diagram = rack.elevation.list(render='svg') # '...' with open('rack-elevation.svg', 'w') as f: f.write(svg_diagram) ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/api.md0000644000175100017510000000502515215017101015062 0ustar00runnerrunner# Core Classes This page documents the core classes that form pynetbox's API surface. ## Overview pynetbox uses a layered architecture, with each layer wrapping the one below: 1. **`Api`** — main entry point; manages the HTTP session, authentication token, and global flags. 2. **`App`** — represents a NetBox application (e.g. `dcim`, `ipam`); attribute access returns endpoints. 3. **`Endpoint`** — represents an individual NetBox API endpoint and exposes CRUD methods. ```python import pynetbox # Create an API connection (Api) nb = pynetbox.api('http://localhost:8000', token='your-token') # Access an app (App) nb.dcim # Access an endpoint (Endpoint) nb.dcim.devices # Call an endpoint method (returns Record / RecordSet) devices = nb.dcim.devices.all() ``` ## Api The `Api` class is the main entry point. It manages the underlying `requests.Session`, holds the authentication token, and provides access to NetBox applications. ::: pynetbox.core.api.Api handler: python options: members: - __init__ - create_token - openapi - status - version - activate_branch show_source: true show_root_heading: true heading_level: 3 ## App The `App` class represents a NetBox application (such as `dcim`, `ipam`, or `circuits`). Accessing an attribute on the `Api` instance returns an `App`; accessing an attribute on an `App` returns an `Endpoint`. ::: pynetbox.core.app.App handler: python options: members: - config - endpoint show_source: true show_root_heading: true heading_level: 3 ## PluginsApp The `PluginsApp` class exposes plugin endpoints under `nb.plugins`. Plugin and endpoint names containing dashes are accessed using underscores (e.g. `/api/plugins/my-plugin/objects/` becomes `nb.plugins.my_plugin.objects`). ::: pynetbox.core.app.PluginsApp handler: python options: members: - installed_plugins show_source: true show_root_heading: true heading_level: 3 ## Relationship to Endpoints Attribute access on an `App` returns an [`Endpoint`](endpoint.md) instance: ```python # nb.dcim is an App # nb.dcim.devices is an Endpoint devices_endpoint = nb.dcim.devices # Endpoint provides CRUD methods all_devices = devices_endpoint.all() device = devices_endpoint.get(1) new_device = devices_endpoint.create(name='test', site=1, device_type=1, role=1) ``` See the [Endpoint reference](endpoint.md) for the full method list. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/branching.md0000644000175100017510000001271715215017101016252 0ustar00runnerrunner# Branching Plugin The [NetBox branching plugin](https://github.com/netboxlabs/netbox-branching) lets you create branches in NetBox, much like a version control system. Changes made within a branch are isolated from the main schema and can later be merged. !!! note "Plugin required" These methods only work when the branching plugin is installed and enabled in your NetBox instance. They have no effect on a stock NetBox installation. ## Registering the Extension pynetbox ships a `BranchingExtension` that wires up the plugin's per-branch detail actions (`merge`, `sync`, `revert`, `archive`) and marks the JSON columns on `ChangeDiff` so they round-trip as Python dicts/lists. Register it with `pynetbox.api(extensions=[...])`: ```python import pynetbox from pynetbox.extensions import BranchingExtension nb = pynetbox.api( "http://localhost:8000", token="your-token-here", extensions=[BranchingExtension], ) ``` Without the extension, `nb.plugins.branching.*` still works for basic CRUD, but the methods described below (`branch.merge()`, etc.) and the JSON-field handling on `Changes` are unavailable. See [Extensions](extensions.md) for the framework's general design. ## Branch Actions With the extension registered, every `Branches` record exposes the plugin's detail actions as direct methods. `sync`, `merge`, and `revert` each return a `Jobs` record for the background job NetBox enqueues; `archive` is synchronous and returns the updated `Branches` record. ```python branch = nb.plugins.branching.branches.create(name="my-branch") # (poll branch.status until 'ready' — see the section further down) # Dry-run a merge to see what would happen. dry_run = branch.merge(commit=False) # Sync the branch from main, then merge it. sync_job = branch.sync(commit=True) merge_job = branch.merge(commit=True) # Revert a previously merged branch. revert_job = branch.revert(commit=True) # Archive a merged branch, deprovisioning its schema. branch.archive() ``` ### Conflict Handling `sync` and `merge` return HTTP 409 when conflicting `ChangeDiff`s exist and have not been acknowledged. This surfaces in pynetbox as a `RequestError`; the response body (with the list of conflicts) is on `error.error`. Resolve the conflicts or pass `acknowledge_conflicts=True` to proceed. ```python import pynetbox try: branch.merge(commit=True) except pynetbox.RequestError as exc: if exc.req.status_code == 409: # exc.error contains the conflicts payload. Inspect, then either # resolve the conflicts and retry, or acknowledge and proceed. branch.merge(commit=True, acknowledge_conflicts=True) else: raise ``` ## Inspecting Changes `ChangeDiff` rows are exposed at `nb.plugins.branching.changes`. The extension marks `diff`, `original_data`, `modified_data`, `current_data`, and `conflicts` as JSON fields so they're returned as plain Python dicts/lists instead of being mangled into nested records. ```python for change in nb.plugins.branching.changes.filter(branch_id=branch.id): print(change.object_type, change.action, change.diff) ``` ## Branchable Models The plugin's `branchable-models` list endpoint enumerates which NetBox models support branching (including plugin-supplied models): ```python for model in nb.plugins.branching.branchable_models.all(): print(model.app_label, model.model) ``` ## Activating a Branch `Api.activate_branch()` is a context manager that adds the `X-NetBox-Branch` header to outgoing requests so they operate against the branch's schema. The header is removed when the `with` block exits. ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-token-here', ) # Look up an existing branch branch = nb.plugins.branching.branches.get(1) # All NetBox operations inside the block run against this branch with nb.activate_branch(branch): sites = nb.dcim.sites.all() # Create, update, and delete calls here only affect the branch's schema ``` The argument must be a branch `Record` (the object returned by the branches endpoint). Passing anything else raises `ValueError`. ## Waiting for a Branch to be Ready Newly created branches are not immediately ready; NetBox runs a background job to provision the branch's schema. Likewise, merges and reverts are asynchronous. The [tenacity](https://github.com/jd/tenacity) library is a convenient way to poll a branch until it reaches a target status: ```bash pip install tenacity ``` ```python from tenacity import retry, retry_if_result, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(30), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_result(lambda ready: not ready), ) def wait_for_branch_status(branch, target_status): """Poll until ``branch`` reaches ``target_status``.""" branch = nb.plugins.branching.branches.get(branch.id) return str(branch.status) == target_status # Create a branch and wait for it to become Ready branch = nb.plugins.branching.branches.create(name='my-branch') wait_for_branch_status(branch, 'Ready') branch = nb.plugins.branching.branches.get(branch.id) print(f"Branch is ready! Status: {branch.status}") ``` The helper above will: 1. Re-fetch the branch and compare its status to the target. 2. Retry with exponential backoff (4–60 seconds between attempts) until the target is reached. 3. Give up after 30 attempts. Exponential backoff strikes a balance between checking frequently enough to catch quick transitions and avoiding excessive load on the NetBox server. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/circuits.md0000644000175100017510000000712715215017101016143 0ustar00runnerrunner# Circuits This page documents the special methods available on Circuits models. Standard CRUD operations follow the patterns described in [Quick Start](getting-started.md) and the [Endpoint reference](endpoint.md). !!! note "Standard API Operations" The standard endpoint methods (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for the available fields and filters on each endpoint. ## Circuit Terminations ### Cable Path Tracing Circuit terminations support cable path tracing via the `paths()` method. It returns every complete cable path that passes through the termination — from origin to destination, including any intermediate cables and pass-through ports. **Example:** ```python # Look up a circuit termination circuit_term = nb.circuits.circuit_terminations.get(circuit_id=123, term_side='A') # All cable paths through this termination for path_info in circuit_term.paths(): print(f"Origin: {path_info['origin']}") print(f"Destination: {path_info['destination']}") print("Path segments:") for segment in path_info['path']: for obj in segment: print(f" - {obj}") # Find what a circuit connects to circuit = nb.circuits.circuits.get(cid='CIRCUIT-001') for term in nb.circuits.circuit_terminations.filter(circuit_id=circuit.id): print(f"\nTermination {term.term_side}:") paths = term.paths() if not paths: print(" No cable paths") continue for path in paths: if path['destination']: print(f" Connected to: {path['destination']}") else: print(" No destination (incomplete path)") ``` **Path Structure:** `paths()` returns a list of dictionaries, one per complete cable path. Each dictionary contains: - `origin`: The starting endpoint of the path (`Record` or `None` if unconnected). - `destination`: The ending endpoint of the path (`Record` or `None` if unconnected). - `path`: A list of path segments. Each segment is a list of `Record` objects representing the components in that segment (cables, terminations, interfaces, etc.). ## Virtual Circuits Virtual circuits represent L2VPN-like connections delivered over one or more underlying physical circuits. **Example:** ```python vcircuit = nb.circuits.virtual_circuits.get(cid='VPLS-001') print(f"Virtual Circuit: {vcircuit.cid}") print(f"Provider Network: {vcircuit.provider_network.name}") print(f"Type: {vcircuit.type.name}") # All terminations for the virtual circuit for term in nb.circuits.virtual_circuit_terminations.filter(virtual_circuit_id=vcircuit.id): print(f"Termination Role: {term.role}") ``` ### Virtual Circuit Termination Path Tracing Virtual circuit terminations also support cable path tracing via `paths()`, with the same structure as circuit terminations. **Example:** ```python vterm = nb.circuits.virtual_circuit_terminations.get( virtual_circuit_id=123, role='hub', ) for path_info in vterm.paths(): print(f"Origin: {path_info['origin']}") print(f"Destination: {path_info['destination']}") print("Path segments:") for segment in path_info['path']: for obj in segment: print(f" - {obj}") # Devices connected via a virtual circuit vcircuit = nb.circuits.virtual_circuits.get(cid='VPLS-001') print(f"Virtual Circuit {vcircuit.cid} connectivity:") for term in nb.circuits.virtual_circuit_terminations.filter(virtual_circuit_id=vcircuit.id): paths = term.paths() if paths and paths[0]['destination']: print(f" {term.role}: {paths[0]['destination']}") ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/core.md0000644000175100017510000000160715215017101015243 0ustar00runnerrunner# Core This page documents the special methods available on Core models. Standard CRUD operations follow the patterns described in [Quick Start](getting-started.md) and the [Endpoint reference](endpoint.md). !!! note "Standard API Operations" The standard endpoint methods (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for the available fields and filters on each endpoint. ## Data Sources ### Sync The `sync` property triggers a synchronization of the data source, enqueuing a job to fetch its files from the configured backend (e.g. git, S3). ::: pynetbox.models.core.DataSources.sync handler: python options: show_source: true **Example:** ```python data_source = nb.core.data_sources.get(name='config-templates') data_source.sync.create() ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/custom-objects.md0000644000175100017510000001552415215017101017257 0ustar00runnerrunner# Custom Objects Plugin The [NetBox Custom Objects plugin](https://github.com/netboxlabs/netbox-custom-objects) lets you define user-defined object types (and their fields) at runtime, then create instances of those types under `/api/plugins/custom-objects//`. !!! note "Plugin required" These methods only work when the custom-objects plugin is installed and enabled in your NetBox instance. They have no effect on a stock NetBox installation. ## Registering the Extension pynetbox ships a `CustomObjectsExtension` that marks the plugin's JSON columns so they round-trip as Python dicts (the fix for [#751](https://github.com/netbox-community/pynetbox/issues/751)), resolves dynamic per-type endpoints, and adds helpers for the plugin's schema preview/apply routes. Register it with `pynetbox.api(extensions=[...])`: ```python import pynetbox from pynetbox.extensions import CustomObjectsExtension nb = pynetbox.api( "http://localhost:8000", token="your-token-here", extensions=[CustomObjectsExtension], ) ``` Without the extension, `nb.plugins.custom_objects.*` still works for basic CRUD, but JSON columns like `related_object_filter` and `schema_document` are mangled into nested `Record` objects (and effectively lost), and per-type endpoints return generic `Record` instances instead of typed `CustomObject` records. See [Extensions](extensions.md) for the framework's general design. ## Custom Object Types and Fields `CustomObjectTypes` and `CustomObjectTypeFields` deserialize the plugin's JSON-typed columns as plain Python dicts/lists: ```python cot = nb.plugins.custom_objects.custom_object_types.get(1) # JSON columns round-trip as plain dicts cot.schema_document # dict cot.fields[0].related_object_filter # dict (fix for #751) cot.fields[0].related_object_type # dict {"id", "app_label", "model"} ``` `fields` is a nested list of `CustomObjectTypeFields` records, so attribute access on each item works as expected. !!! note "`related_object_types` (plural)" On polymorphic Object/MultiObject fields the server returns `related_object_types` as a list of `{id, app_label, model}` dicts. This field is not marked `JsonField`, so pynetbox wraps each list item in a base `Record`, so `field.related_object_types[0].app_label` works, but `isinstance(field.related_object_types[0], dict)` is `False` — unlike the singular `related_object_type`, which is marked `JsonField` and round-trips as a plain dict. (Marking a list-valued column `JsonField` keeps its items as plain dicts.) ## Custom Objects (per-type endpoints) Each Custom Object Type exposes a list endpoint at `/api/plugins/custom-objects//`. Attribute access on `nb.plugins.custom_objects` returns a typed `CustomObject` record for any endpoint name (the underscore-to-dash convention applies, so a COT with slug `cidr-list` is reached as `nb.plugins.custom_objects.cidr_list`): ```python for obj in nb.plugins.custom_objects.cidr_list.all(): print(obj.name, obj.custom_object_type.slug) # Create via the dynamic endpoint new = nb.plugins.custom_objects.cidr_list.create(name="ours", prefix=42) ``` `CustomObject.custom_object_type` resolves to a `CustomObjectTypes` record, so `obj.custom_object_type.slug` works without an extra fetch when the per-type serializer includes it. ### Slugs that contain underscores Attribute access converts underscores to dashes, which is correct for almost every endpoint but breaks when an endpoint slug genuinely contains an underscore — e.g. a slug of `my_first_custom_object` would be requested as `.../custom-objects/my-first-custom-object/` and 404. Use `App.endpoint()` to pass the slug verbatim (the fix for [#715](https://github.com/netbox-community/pynetbox/issues/715)): ```python # Attribute access — underscores become dashes: nb.plugins.custom_objects.my_first_custom_object # → .../custom-objects/my-first-custom-object/ # endpoint() — slug used as-is, underscores preserved: nb.plugins.custom_objects.endpoint("my_first_custom_object").all() # → .../custom-objects/my_first_custom_object/ ``` `endpoint()` is a per-call escape hatch: it returns an ordinary `Endpoint` (same CRUD methods, same typed `CustomObject` records) and changes nothing about the default attribute-access behaviour. It works on any `App`, not just plugins. ## Linked Objects `linked-objects/` reports which custom objects link to a given NetBox object via an Object/MultiObject field: ```python hits = nb.plugins.custom_objects.linked_objects.filter( object_type="dcim.device", object_id=5, ) for row in hits: print(row.custom_object_type["slug"], row.field_name, row.object["id"]) ``` Both `custom_object_type` and `object` come back as plain dicts — the target type isn't fixed (it can be any NetBox model), so the extension intentionally doesn't try to wrap them in typed records. ## Schema Preview and Apply The plugin exposes two non-CRUD endpoints for declarative schema management: * **`POST /schema/preview/`** — diff a schema document against the current DB, no changes applied. * **`POST /schema/apply/`** — apply the diff (optionally allowing destructive operations). pynetbox wraps both as module-level helpers: ```python from pynetbox.extensions.custom_objects import schema_apply, schema_preview doc = { "schema_version": "1", "types": [ { "name": "my_cot", "slug": "my-cot", "fields": [ {"id": 1, "name": "label", "type": "text", "primary": True, "required": True}, ], } ], } # Dry-run: returns {"diffs": [...]} diff = schema_preview(nb, doc) # Apply: returns {"applied": True, "diffs": [...]} result = schema_apply(nb, doc, allow_destructive=False) ``` The plugin returns HTTP 409 when the document contains destructive changes (field removals) and `allow_destructive=False`; pynetbox surfaces this as a `RequestError`. The conflict payload is on `exc.error` and includes the destructive COT slugs: ```python import pynetbox try: schema_apply(nb, doc) except pynetbox.RequestError as exc: if exc.req.status_code == 409: # exc.error: {"error": "destructive_changes", "destructive_slugs": [...]} # After reviewing, re-apply with allow_destructive=True. schema_apply(nb, doc, allow_destructive=True) else: raise ``` Other server-side errors map to `RequestError` as well: 400 for invalid documents, circular COT dependencies, or unresolvable FK targets; 403 if the user lacks `add_customobjecttype` and `change_customobjecttype` permissions. ## Generic Foreign Key resolution The extension registers content-type mappings for `netbox_custom_objects.customobjecttype` and `netbox_custom_objects.customobjecttypefield`, so any other endpoint that references these models via a generic FK (for example, a tag scoped to a Custom Object Type) deserializes the nested object into the right typed record. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/dcim.md0000644000175100017510000001203315215017101015222 0ustar00runnerrunner# DCIM This page documents the special methods available on DCIM models. Standard CRUD operations follow the patterns described in [Quick Start](getting-started.md) and the [Endpoint reference](endpoint.md). !!! note "Standard API Operations" The standard endpoint methods (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for the available fields and filters on each endpoint. ## Devices ### NAPALM Integration The `napalm` property exposes the `/napalm/` detail route on a device, providing live data fetched from the device through NAPALM. This requires NAPALM to be configured on the NetBox server. ::: pynetbox.models.dcim.Devices.napalm handler: python options: show_source: true **Example:** ```python device = nb.dcim.devices.get(name='router1') # Device facts facts = device.napalm.list(method='get_facts') # Interfaces interfaces = device.napalm.list(method='get_interfaces') # ARP table arp = device.napalm.list(method='get_arp_table') ``` ### Config Rendering The `render_config` property renders a device's configuration from its assigned config template and config contexts. ::: pynetbox.models.dcim.Devices.render_config handler: python options: show_source: true **Example:** ```python device = nb.dcim.devices.get(name='switch1') config = device.render_config.create() print(config) ``` ## Racks ### Rack Units The `units` property returns information about each rack unit and the device installed in it. ::: pynetbox.models.dcim.Racks.units handler: python options: show_source: true **Example:** ```python rack = nb.dcim.racks.get(name='RACK-01') for unit in rack.units.list(): if unit.device: print(f"U{unit.name}: {unit.device.name}") else: print(f"U{unit.name}: Empty") ``` ### Rack Elevation The `elevation` property supports both JSON and SVG output. By default it returns a list of rack-unit records; passing `render='svg'` returns a rendered SVG diagram as a string. ::: pynetbox.models.dcim.Racks.elevation handler: python options: show_source: true **Examples:** ```python rack = nb.dcim.racks.get(name='RACK-01') # As JSON (list of RU objects) elevation_data = rack.elevation.list() # As an SVG diagram svg_diagram = rack.elevation.list(render='svg') with open('rack-elevation.svg', 'w') as f: f.write(svg_diagram) ``` ## Cable Tracing Several DCIM models expose a `trace()` method that traces a cable end-to-end, returning each hop of the path. **Supported models:** - Interfaces - ConsolePorts - ConsoleServerPorts - PowerPorts - PowerOutlets - PowerFeeds The return value is a flat list arranged as `[a_terminations, cable, b_terminations, a_terminations, cable, b_terminations, ...]`, where each `*_terminations` entry is a list of Record objects and each `cable` entry is either a `Cables` record or `None` for an incomplete path. **Example:** ```python interface = nb.dcim.interfaces.get(name='eth0', device='switch1') trace_result = interface.trace() for item in trace_result: if isinstance(item, list): # Terminations for term in item: print(f" Termination: {term}") else: # Cable (or None for an incomplete path) if item: print(f" Cable: {item.id} - {item.label}") else: print(" No cable") # Console port console = nb.dcim.console_ports.get(name='Console', device='router1') console_trace = console.trace() # Power connection power_port = nb.dcim.power_ports.get(name='PSU1', device='server1') power_trace = power_port.trace() ``` ## Cable Path Tracing (Pass-Through Ports) Front ports and rear ports use the `paths()` method instead of `trace()` because a single port can participate in multiple complete cable paths. **Supported models:** - FrontPorts - RearPorts **Example:** ```python front_port = nb.dcim.front_ports.get(name='FrontPort1', device='patch-panel-1') for path_info in front_port.paths(): print(f"Origin: {path_info['origin']}") print(f"Destination: {path_info['destination']}") print("Path segments:") for segment in path_info['path']: for obj in segment: print(f" - {obj}") # Rear ports work the same way rear_port = nb.dcim.rear_ports.get(name='RearPort1', device='patch-panel-1') rear_paths = rear_port.paths() if rear_paths: first_path = rear_paths[0] if first_path['origin']: print(f"Cable path starts at: {first_path['origin']}") if first_path['destination']: print(f"Cable path ends at: {first_path['destination']}") ``` **Path Structure:** `paths()` returns a list of dictionaries, one per complete cable path. Each dictionary contains: - `origin`: The starting endpoint of the path (`Record` or `None` if unconnected). - `destination`: The ending endpoint of the path (`Record` or `None` if unconnected). - `path`: A list of path segments. Each segment is a list of `Record` objects representing the components in that segment (cables, terminations, etc.). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9467616 pynetbox-7.8.0/docs/development/0000755000175100017510000000000015215017112016311 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/development/getting-started.md0000644000175100017510000000573615215017101021751 0ustar00runnerrunner# Getting Started This guide walks through setting up a pynetbox development environment and running the test suite. ## Development Environment 1. Fork the pynetbox repository on GitHub. 2. Clone your fork locally. 3. Create a virtual environment and install the runtime and development dependencies: ```bash python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt pip install -r requirements-dev.txt pip install -e . ``` ## Running Tests pynetbox uses [pytest](https://docs.pytest.org/) for both unit tests and integration tests. Linting is handled with [ruff](https://docs.astral.sh/ruff/). Before submitting changes, make sure `ruff check` passes: ```bash ruff check pynetbox/ tests/ ``` ### Unit Tests Unit tests mock HTTP responses with JSON fixtures from `tests/fixtures/` and have no external dependencies: ```bash pytest tests/unit ``` ### Integration Tests Integration tests run against real NetBox instances. The suite uses [pytest-docker](https://pypi.org/project/pytest-docker/) to spin up [netbox-docker](https://github.com/netbox-community/netbox-docker) containers, so Docker must be installed and running. ```bash pytest tests/integration ``` You can choose which NetBox versions to test against with `--netbox-versions` (comma-separated): ```bash pytest tests/integration --netbox-versions 4.3,4.4,4.5 ``` To leave the containers running after the tests finish (useful for debugging): ```bash pytest tests/integration --no-cleanup ``` To run integration tests against an existing NetBox instance instead of starting containers: ```bash pytest tests/integration -p no:docker --url-override http://localhost:8000 ``` ### Running Specific Tests ```bash # A specific test file pytest tests/unit/test_endpoint.py # A specific test function pytest tests/unit/test_endpoint.py::TestEndpoint::test_get_by_id # All tests whose name matches a pattern pytest -k "filter" ``` ### Test Coverage ```bash pytest --cov=pynetbox tests/ ``` ## Submitting Pull Requests Once your changes are complete and all tests pass, commit them and push to your fork. Use descriptive commit messages that explain the *why* rather than the *what*. Prefix the subject line with `Fixes #NNN:` or `Closes #NNN:` referencing the relevant issue so that GitHub automatically closes it when the commit merges: ```bash git commit -m "Closes #1234: Add IPv5 support" git push origin ``` Open a pull request against the pynetbox repository describing what changed and why. A maintainer will review the PR and either merge it or request changes. You can address review feedback by pushing additional commits to your branch — the pull request updates automatically. !!! warning Pull requests are only accepted for **approved** issues. If the issue you're working on hasn't been triaged and approved by a maintainer yet, please hold off — there's no guarantee it will be accepted. (The one exception is trivial documentation fixes and similar non-critical changes.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/development/index.md0000644000175100017510000000561415215017101017746 0ustar00runnerrunner# Development Thanks for your interest in contributing to pynetbox! This page covers the project's branch layout, how to propose changes, and where to get help. ## The Code pynetbox is maintained on [GitHub](https://github.com/netbox-community/pynetbox), which also serves as the project's primary discussion forum. You'll need a (free) GitHub account to file issues or open pull requests. Most contributors begin by forking the repository under their own account. There are two permanent branches: - **`master`** — Active development for the next patch release. Pull requests should target this branch unless they introduce breaking changes that must wait for the next major release. - **`feature`** — New feature work staged for the next major release. The source tree is organized into the following directories: - `pynetbox/core/` — Core HTTP, request, response, and endpoint plumbing. - `pynetbox/models/` — Model classes (custom `Record` subclasses) for the various NetBox apps. - `tests/` — Unit and integration tests. - `docs/` — User documentation (this site). ## Proposing Changes All substantial changes are tracked via GitHub issues. Feature requests, bug reports, and other proposals must be filed as issues and approved by a maintainer before work begins. This keeps the design rationale for each change discoverable in one place. To file a new feature request or bug report, select the appropriate issue template. Once your issue has been triaged and accepted, you're welcome to submit a pull request implementing the change. !!! note Please avoid starting work on a proposal before it has been accepted. Not every proposal will be approved, and we'd hate for your time to be wasted on a change that isn't merged. ## Getting Help Two forums are available for development questions: - [GitHub Discussions](https://github.com/netbox-community/pynetbox/discussions) — The preferred forum for general discussion and support. Ideal for shaping a feature idea before filing an issue. - [#netbox on the NetDev Community Slack](https://netdev.chat) — Useful for quick chats. Avoid using it for anything that needs to be referenced later, as the chat history is not retained indefinitely. !!! note Don't use GitHub issues to ask for help — those are reserved for proposed code changes only. ## Governance pynetbox follows the benevolent-dictator model of governance, with the lead maintainer ultimately responsible for all changes to the codebase. Community contributions are welcomed and encouraged, but the lead maintainer's primary role is to ensure the project's long-term maintainability and its continued focus on its core mission. ## Licensing pynetbox is released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0). This is a permissive license that allows unrestricted redistribution of the code. Note that all submissions to the project are subject to the same license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/development/release-checklist.md0000644000175100017510000000424015215017101022220 0ustar00runnerrunner# Release Checklist This page outlines the steps to prepare and publish a new pynetbox release. The package version is derived from the git tag at release time by [setuptools_scm](https://github.com/pypa/setuptools_scm), so the source tree does not carry a hardcoded version in `setup.py`. ## Pre-Release Tasks 1. Ensure all tests pass: ```bash ruff check pynetbox/ tests/ pytest ``` 2. Update `__version__` in `pynetbox/__init__.py` to the new version. 3. Update [`CHANGELOG.md`](https://github.com/netbox-community/pynetbox/blob/master/CHANGELOG.md) and `docs/release-notes.md` with a summary of changes, referencing the relevant issue and PR numbers. 4. Check supported NetBox versions: - Review the latest [netbox-docker releases](https://github.com/netbox-community/netbox-docker/releases). - Update `DEFAULT_NETBOX_VERSIONS` and the `get_netbox_docker_version_tag` function in `tests/integration/conftest.py` if there are new NetBox versions or new netbox-docker tag mappings. - Widen the `netbox` matrix in `.github/workflows/py3.yml` to cover the supported range. ## Release Tasks 1. Create a release branch from `master`: ```bash git checkout master git pull git checkout -b release/vX.Y.Z ``` 2. Commit the version bump and changelog updates: ```bash git commit -m "Prepare release vX.Y.Z" ``` 3. Open a pull request to merge the release branch into `master`. 4. Once the PR is merged, publish a GitHub release: 1. Go to the [Releases page](https://github.com/netbox-community/pynetbox/releases) on GitHub. 2. Click **Draft a new release**. 3. Create a new tag named `vX.Y.Z` against `master`. 4. Paste the relevant changelog section into the release notes. 5. Publish the release. Publishing triggers the [`publish.yml`](https://github.com/netbox-community/pynetbox/blob/master/.github/workflows/publish.yml) workflow, which builds the sdist and wheel and uploads them to PyPI. ## Supported NetBox Versions pynetbox aims to support the current and previous two minor versions of NetBox. The supported versions are defined in `tests/integration/conftest.py` and should be reviewed and updated each release cycle. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/endpoint.md0000644000175100017510000000461715215017101016137 0ustar00runnerrunner# Endpoint `Endpoint` objects provide CRUD operations for NetBox API endpoints. They are created automatically when you access an attribute on an [`App`](api.md#app) instance. ## Overview ```python import pynetbox nb = pynetbox.api('http://localhost:8000', token='your-token') # Accessing an attribute on an App returns an Endpoint devices = nb.dcim.devices # Endpoint # Endpoint methods perform CRUD operations all_devices = devices.all() device = devices.get(1) filtered = devices.filter(site='headquarters') new_device = devices.create(name='test', site=1, device_type=1, role=1) ``` ## Slugs with underscores Attribute access converts underscores to dashes (`nb.dcim.ip_addresses` → the `ip-addresses` endpoint), since Python attribute names can't contain dashes. For the rare endpoint whose URL slug legitimately contains an underscore (e.g. some custom-objects plugin types), use `App.endpoint()` to pass the slug verbatim: ```python # Attribute access — underscores become dashes: nb.plugins.custom_objects.my_object # → .../custom-objects/my-object/ # endpoint() — slug used as-is: nb.plugins.custom_objects.endpoint("my_object").all() # → .../custom-objects/my_object/ ``` ## Endpoint Class ::: pynetbox.core.endpoint.Endpoint handler: python options: members: - all - choices - count - create - delete - filter - get - update show_source: true show_root_heading: true heading_level: 3 ## DetailEndpoint Class `DetailEndpoint` represents a detail route on an existing record (e.g. `/api/ipam/prefixes/{id}/available-ips/`). It is returned by model-specific properties such as `Prefixes.available_ips`, not constructed directly. ::: pynetbox.core.endpoint.DetailEndpoint handler: python options: members: - create - list show_source: true show_root_heading: true heading_level: 3 ## ROMultiFormatDetailEndpoint Class A read-only detail endpoint that supports multiple response formats. Used for endpoints (such as rack elevation) that can return either structured JSON or raw content like SVG. ::: pynetbox.core.endpoint.ROMultiFormatDetailEndpoint handler: python options: members: - list show_source: true show_root_heading: true heading_level: 3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/exceptions.md0000644000175100017510000000251015215017101016466 0ustar00runnerrunner# Exceptions pynetbox raises a small set of dedicated exceptions in response to error conditions. They all live in `pynetbox.core.query` and are re-exported at the top level of the `pynetbox` package, so they can also be imported as `pynetbox.RequestError`, `pynetbox.ContentError`, `pynetbox.AllocationError`, and `pynetbox.ParameterValidationError`. ## RequestError ::: pynetbox.core.query.RequestError handler: python options: show_source: true show_root_heading: true heading_level: 3 ## ContentError ::: pynetbox.core.query.ContentError handler: python options: show_source: true show_root_heading: true heading_level: 3 ## AllocationError ::: pynetbox.core.query.AllocationError handler: python options: show_source: true show_root_heading: true heading_level: 3 ## ParameterValidationError ::: pynetbox.core.query.ParameterValidationError handler: python options: show_source: true show_root_heading: true heading_level: 3 ## Example ```python import pynetbox nb = pynetbox.api('http://localhost:8000', token='your-token') try: nb.dcim.devices.create(name='destined-for-failure') except pynetbox.RequestError as e: # The error returned by the server is exposed as e.error. print(e.error) ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/extensions.md0000644000175100017510000001036315215017101016511 0ustar00runnerrunner# Extensions pynetbox's extension framework lets you (or a plugin author) register custom `Record` subclasses, JSON-field markers, and content-type mappings for NetBox plugins — without modifying pynetbox itself. Extensions are opt-in and passed in explicitly when constructing the API client: ```python import pynetbox from pynetbox.extensions import BranchingExtension nb = pynetbox.api( "http://localhost:8000", token="your-token-here", extensions=[BranchingExtension], ) ``` Plugin endpoints that an extension covers are automatically deserialized into the extension's `Record` subclasses, and any content types the extension registers participate in generic-FK resolution across the whole client. ## Shipped Extensions pynetbox ships a small set of extensions for popular NetBox plugins. These live under `pynetbox.extensions`. | Plugin | Extension | Docs | |---|---|---| | [netbox-branching](https://github.com/netboxlabs/netbox-branching) | `pynetbox.extensions.BranchingExtension` | [Branching](branching.md) | | [netbox-custom-objects](https://github.com/netboxlabs/netbox-custom-objects) | `pynetbox.extensions.CustomObjectsExtension` | [Custom Objects](custom-objects.md) | If you write or maintain an extension for a NetBox plugin that's widely used and want to upstream it, open an issue on the pynetbox repo. ## Writing your own Extension An extension is any object exposing three attributes. Subclassing `pynetbox.Extension` is optional — it just gives you a typed base to target. ```python from pynetbox import Extension from pynetbox.core.endpoint import DetailEndpoint from pynetbox.core.response import JsonField, Record class Notes(Record): # Mark a JSON dict column so pynetbox doesn't mangle it into a nested Record. metadata = JsonField @property def render(self): # Expose a per-object sub-route like /notes/{id}/render/. return DetailEndpoint(self, "render") class NotesModels: # Attribute names match the title-cased endpoint name. pynetbox looks up # ``getattr(models, "Notes", Record)`` for the endpoint # ``nb.plugins.notes.notes``. Notes = Notes class NotesExtension(Extension): plugin_name = "notes" # matches nb.plugins.notes models = NotesModels content_types = { # optional; merged into the GFK mapper "notes.note": Notes, } ``` For real shipped extensions, see `pynetbox/extensions/branching.py` and `pynetbox/extensions/custom_objects.py`. ### `plugin_name` The plugin's slug as it appears under `nb.plugins`. Attribute access uses underscores while the URL slug uses dashes, matching pynetbox's existing convention: `plugin_name = "custom_objects"` matches both `nb.plugins.custom_objects` and `/api/plugins/custom-objects/`. ### `models` A namespace — a module, a class, or any object — from which `Record` subclasses are looked up by title-cased endpoint name. For `nb.plugins..foo_bars` pynetbox does `getattr(models, "FooBars", Record)`, so name your subclasses to match. Common things to put on a Record subclass: - **`JsonField` markers** for columns that are JSON dicts/lists. Without this, pynetbox tries to coerce a dict-typed field into a nested `Record` and the data is lost. (See `Changes` in `pynetbox/extensions/branching.py` for an example.) - **`DetailEndpoint` properties** for per-object sub-routes like `/foo/{id}/run/` or `/foo/{id}/render/`. - **Custom methods** for plugin-specific actions, returning whatever record type the response represents. ### `content_types` (optional) A mapping of NetBox content-type strings (e.g. `"netbox_branching.branch"`) to your `Record` subclasses. These are merged into a per-instance copy of pynetbox's built-in `CONTENT_TYPE_MAPPER`, so polymorphic nested objects (generic foreign keys) deserialize into your classes. ### Caveats - **Scope is limited to plugins** under `nb.plugins.*`. Extensions cannot override built-in apps (dcim, ipam, etc.). - **No auto-discovery.** Extensions are always passed in explicitly to `pynetbox.api(extensions=[...])`. If you want a third-party package to ship an extension, document the import path users should pass in. - **One extension per `plugin_name`** per API instance. If two extensions claim the same plugin name, the later one wins. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/getting-started.md0000644000175100017510000002137415215017101017423 0ustar00runnerrunner# Quick Start This guide walks through the basics of using pynetbox to interact with NetBox. ## Basic Connection Import `pynetbox` and create an API connection: ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) ``` ### Connection Parameters `pynetbox.api()` accepts the following parameters: - **`url`** (required): Base URL of your NetBox instance, including scheme (`http://` or `https://`). - **`token`** (optional): API authentication token. Required for write operations and for endpoints that are not public. - **`threading`** (optional, default `False`): Enable multithreaded page fetching for `.all()` and `.filter()`. See [Threading](advanced.md#threading). - **`strict_filters`** (optional, default `False`): Validate filter parameters against NetBox's OpenAPI specification. See [Filter Validation](advanced.md#filter-validation). ```python nb = pynetbox.api( 'http://localhost:8000', token='your-token-here', threading=True, strict_filters=True, ) ``` ## API Structure pynetbox mirrors NetBox's app structure. Each NetBox app is exposed as an attribute of the API object: ```python nb.circuits # Circuit management nb.core # Core objects (data sources, jobs, object changes) nb.dcim # Data Center Infrastructure Management nb.extras # Tags, custom fields, webhooks, etc. nb.ipam # IP Address Management nb.tenancy # Tenants and contacts nb.users # Users and permissions nb.virtualization # Virtual machines and clusters nb.vpn # VPN tunnels and terminations nb.wireless # Wireless LANs and links ``` Each app exposes endpoints that map to NetBox API endpoints. Dashes in endpoint names are converted to underscores: ```python nb.dcim.devices nb.dcim.sites nb.dcim.racks nb.ipam.ip_addresses # /api/ipam/ip-addresses/ nb.ipam.prefixes ``` ### Plugin Endpoints Endpoints provided by NetBox plugins are accessible under the `plugins` namespace. As with built-in endpoints, dashes in plugin or endpoint names are converted to underscores: ```python # /api/plugins/my-plugin/objects/ nb.plugins.my_plugin.objects.all() ``` ## Querying Data ### Getting All Objects Use `.all()` to retrieve every object from an endpoint: ```python for device in nb.dcim.devices.all(): print(device.name) ``` !!! warning "RecordSet is a one-shot iterator" `.all()` and `.filter()` return a `RecordSet`, which can only be iterated once. To iterate the results multiple times, materialize them with `list()`: ```python devices = list(nb.dcim.devices.all()) ``` ### Filtering Objects Use `.filter()` to query objects matching one or more criteria. Keyword arguments correspond to filter parameters supported by the NetBox endpoint: ```python # All devices with a specific role leaf_switches = nb.dcim.devices.filter(role='leaf-switch') # Multiple filters (AND) devices = nb.dcim.devices.filter( site='headquarters', status='active', role='access-switch', ) # Filter on a custom field devices = nb.dcim.devices.filter(cf_environment='production') # Freeform search (passed as ?q=...) results = nb.dcim.devices.filter('rack1') ``` !!! note "Invalid filters do not raise an error by default" NetBox silently ignores unrecognized filter parameters, which means a typo can quietly return the entire table. Enable [strict filter validation](advanced.md#filter-validation) to catch these errors early. ### Getting a Single Object Use `.get()` to retrieve a single object. It returns `None` if no match is found and raises `ValueError` if more than one record matches: ```python # By ID device = nb.dcim.devices.get(1) # By a unique field device = nb.dcim.devices.get(name='spine1') # Combining filters when a single field is not unique location = nb.dcim.locations.get(site='site-1', name='Row 1') # get() returns None if no match is found device = nb.dcim.devices.get(name='nonexistent') if device is None: print("Device not found") ``` ### Counting Objects Use `.count()` to retrieve only the count of matching objects without fetching them: ```python total = nb.dcim.devices.count() active = nb.dcim.devices.count(status='active') ``` ## Working with Records ### Accessing Attributes API fields are exposed as attributes on `Record` objects: ```python device = nb.dcim.devices.get(1) print(device.name) print(device.serial) print(device.device_type.model) # Nested object print(device.site.name) ``` ### Inspecting Available Attributes ```python device = nb.dcim.devices.get(1) # As a dict print(dict(device)) # Or get the data structure ready for an API payload print(device.serialize()) ``` ## Creating Objects Use `.create()` to create new objects. Pass field values as keyword arguments: ```python # Create a site new_site = nb.dcim.sites.create( name='new-datacenter', slug='new-datacenter', status='planned', ) # Create a device (NetBox 3.6+ uses `role`; pre-3.6 used `device_role`) new_device = nb.dcim.devices.create( name='new-switch', device_type=1, site=new_site.id, role=5, ) # Create an IP address with assignment to an interface new_ip = nb.ipam.ip_addresses.create( address='10.0.0.1/24', status='active', assigned_object_type='dcim.interface', assigned_object_id=123, ) ``` To create multiple objects in a single request, pass a list of dicts: ```python nb.dcim.sites.create([ {'name': 'site-1', 'slug': 'site-1'}, {'name': 'site-2', 'slug': 'site-2'}, ]) ``` ## Updating Objects There are two ways to update a single object. ### Modify attributes and `save()` ```python device = nb.dcim.devices.get(1) device.serial = 'ABC123' device.asset_tag = 'ASSET001' device.save() ``` `save()` sends a PATCH containing only the fields that have changed and returns `True` if the request succeeded. ### Pass a dict to `update()` ```python device = nb.dcim.devices.get(1) device.update({ 'serial': 'ABC123', 'asset_tag': 'ASSET001', }) ``` ### Bulk Updates To update multiple records in a single request, call `.update()` on the endpoint with a list of records or dicts. Each dict must include an `id`: ```python devices = nb.dcim.devices.filter(site='test1') for device in devices: device.status = 'active' nb.dcim.devices.update(devices) ``` `RecordSet` also supports a chainable form: ```python nb.dcim.devices.filter(site_id=1).update(status='active') ``` ## Deleting Objects Use `.delete()` to remove an object: ```python device = nb.dcim.devices.get(1) device.delete() ``` To delete multiple objects in a single request, pass a list (or `RecordSet`) of records or IDs to the endpoint's `.delete()`: ```python nb.dcim.devices.delete([1, 2, 3]) # Or, delete every record matching a filter: nb.dcim.devices.filter(status='offline').delete() ``` ## Working with Choices Use `.choices()` to retrieve the valid values for each choice field on an endpoint. The return value is a dict keyed by field name, with each value being a list of `{value, label}` entries: ```python choices = nb.dcim.devices.choices() for choice in choices['status']: print(choice['value'], choice['label']) # active Active # planned Planned # staged Staged # ... ``` ## Pagination NetBox paginates list responses, but pynetbox fetches subsequent pages automatically as you iterate the result set. You can override the page size with `limit`: ```python # Fetch 100 records per page (instead of the server default) devices = nb.dcim.devices.filter(limit=100) # Start at a specific offset (requires a positive limit) devices = nb.dcim.devices.filter(limit=100, offset=200) ``` ## Error Handling pynetbox raises specific exceptions for different failure modes: ```python import pynetbox try: nb.dcim.devices.create( name='test-device', device_type=1, site=1, role=1, ) except pynetbox.RequestError as e: # HTTP error from NetBox (4xx/5xx) print(f"Request failed: {e.error}") except pynetbox.AllocationError as e: # No room left in an available-ips / available-prefixes pool print(f"Allocation failed: {e}") except pynetbox.ContentError as e: # Server returned a non-JSON response (usually a misconfigured URL) print(f"Content error: {e}") except pynetbox.ParameterValidationError as e: # strict_filters=True and a filter parameter is invalid print(f"Invalid filter: {e}") ``` ## Next Steps - [API Reference](api.md) — detailed documentation of the core classes. - [Advanced Topics](advanced.md) — threading, filter validation, custom sessions, file uploads. - Endpoint-specific helpers: - [DCIM](dcim.md) - [IPAM](ipam.md) - [Circuits](circuits.md) - [Virtualization](virtualization.md) - [NetBox REST API documentation](https://demo.netbox.dev/api/docs/) — the canonical reference for fields and filters. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/index.md0000644000175100017510000000545215215017101015424 0ustar00runnerrunner# pynetbox Documentation Python API client library for [NetBox](https://github.com/netbox-community/netbox). ## Overview pynetbox is a Python client library that provides a simple, Pythonic interface to NetBox's REST API. It handles authentication, pagination, and serialization, allowing you to read and write NetBox data without dealing with raw HTTP requests. ## Features - **Intuitive API**: NetBox apps and endpoints are exposed as Python attributes (`nb.dcim.devices`). - **Full CRUD support**: Create, read, update, and delete NetBox objects. - **Threading**: Optional parallel page fetching for large result sets. - **Filter validation**: Optional strict validation of filter parameters against NetBox's OpenAPI specification. - **Custom sessions**: Drop in your own `requests.Session` for custom SSL, timeouts, and retries. - **Branching plugin support**: Context manager for the NetBox branching plugin. - **Comprehensive coverage**: Supports every built-in NetBox app (DCIM, IPAM, Circuits, Virtualization, Tenancy, Extras, Users, Wireless, VPN, Core) plus plugin endpoints. ## NetBox Version Compatibility !!! warning "Version Requirements" pynetbox 6.7 and later support only NetBox 3.3 and above. Each pynetbox version has been tested against the corresponding NetBox versions: | pynetbox Version | NetBox Version | |:----------------:|:------------------:| | 7.7.0 | 4.3, 4.4, 4.5, 4.6 | | 7.6.1 | 4.5 | | 7.6.0 | 4.5 | | 7.5.0 | 4.1, 4.2, 4.3, 4.4 | | 7.4.1 | 4.0.6 | | 7.4.0 | 4.0 | | 7.3.x | 3.7 | | 7.2.0 | 3.6 | | 7.1.0 | 3.5 | | 7.0.x | 3.3, 3.4 | ## Quick Example ```python import pynetbox # Initialize the API connection nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) # Query all devices for device in nb.dcim.devices.all(): print(device.name) # Filter devices leaf_switches = nb.dcim.devices.filter(role='leaf-switch') # Get a specific device device = nb.dcim.devices.get(name='spine1') # Create a new device new_device = nb.dcim.devices.create( name='new-device', device_type=1, site=1, role=1, ) # Update a device device.serial = 'ABC123' device.save() ``` ## Getting Help - **GitHub Issues**: Report bugs or request features at [github.com/netbox-community/pynetbox/issues](https://github.com/netbox-community/pynetbox/issues). - **GitHub Discussions**: General questions and discussion at [github.com/netbox-community/pynetbox/discussions](https://github.com/netbox-community/pynetbox/discussions). - **Source Code**: [github.com/netbox-community/pynetbox](https://github.com/netbox-community/pynetbox). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/installation.md0000644000175100017510000000467115215017101017020 0ustar00runnerrunner# Installation ## Requirements - **Python**: 3.10 or higher - **NetBox**: 3.3 or higher (for pynetbox 6.7+) - **Dependencies**: - `requests>=2.20.0,<3.0` - `packaging` ## Installation Methods ### Install from PyPI (Recommended) ```bash pip install pynetbox ``` ### Install from Source To install the latest development version directly from GitHub: ```bash git clone https://github.com/netbox-community/pynetbox.git cd pynetbox pip install -e . ``` ### Using a Virtual Environment It is strongly recommended to install pynetbox into a virtual environment so its dependencies are isolated from your system Python: ```bash # Create a virtual environment python3 -m venv venv # Activate it (Linux/macOS) source venv/bin/activate # Activate it (Windows) venv\Scripts\activate # Install pynetbox pip install pynetbox ``` ## Verifying Installation After installation, verify that pynetbox is importable and check its version: ```python import pynetbox print(pynetbox.__version__) ``` You can also test connectivity to your NetBox instance: ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='your-api-token-here' ) # Check NetBox version print(nb.version) # Check API status print(nb.status()) ``` ## Upgrading To upgrade to the latest version: ```bash pip install --upgrade pynetbox ``` To install a specific version: ```bash pip install pynetbox==7.7.0 ``` ## Development Installation If you intend to develop or contribute to pynetbox: ```bash # Clone the repository git clone https://github.com/netbox-community/pynetbox.git cd pynetbox # Create and activate a virtual environment python3 -m venv venv source venv/bin/activate # Install runtime and development dependencies pip install -r requirements.txt pip install -r requirements-dev.txt # Install pynetbox in editable mode pip install -e . ``` The integration test suite uses [pytest-docker](https://pypi.org/project/pytest-docker/) to spin up NetBox in containers, so Docker must be installed and running to execute integration tests. Unit tests have no such requirement. For more information on running the test suite, see the [Development Guide](development/index.md). ## Next Steps - Read the [Quick Start](getting-started.md) for an introduction to basic usage. - Explore the [API Reference](api.md) for detailed documentation of core classes. - Review [Advanced Topics](advanced.md) for threading, filter validation, custom sessions, and file uploads. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/ipam.md0000644000175100017510000001042615215017101015240 0ustar00runnerrunner# IPAM This page documents the special methods available on IPAM models. Standard CRUD operations follow the patterns described in [Quick Start](getting-started.md) and the [Endpoint reference](endpoint.md). !!! note "Standard API Operations" The standard endpoint methods (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for the available fields and filters on each endpoint. ## Prefixes ### Available IPs The `available_ips` property exposes the prefix's `available-ips` detail route, used to list addresses that are not yet allocated and to provision new ones from the available pool. ::: pynetbox.models.ipam.Prefixes.available_ips handler: python options: show_source: true **Examples:** ```python prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/24') # List available IP addresses available = prefix.available_ips.list() # [10.0.0.1/24, 10.0.0.2/24, 10.0.0.3/24, ...] # Provision a single IP from the pool new_ip = prefix.available_ips.create() # Provision multiple IPs new_ips = prefix.available_ips.create([{} for _ in range(5)]) # Provision an IP with specific attributes new_ip = prefix.available_ips.create({ 'dns_name': 'server01.example.com', 'description': 'Web Server', 'status': 'active', }) ``` ### Available Prefixes The `available_prefixes` property exposes the parent prefix's `available-prefixes` detail route, used to list and carve out child prefixes. ::: pynetbox.models.ipam.Prefixes.available_prefixes handler: python options: show_source: true **Examples:** ```python prefix = nb.ipam.prefixes.get(prefix='10.0.0.0/16') # List available child prefixes available = prefix.available_prefixes.list() # [10.0.1.0/24, 10.0.2.0/23, 10.0.4.0/22, ...] # Carve out a single child prefix (prefix_length is required) new_prefix = prefix.available_prefixes.create({ 'prefix_length': 24, 'status': 'active', 'description': 'Server subnet', }) # Carve out multiple child prefixes new_prefixes = prefix.available_prefixes.create([ {'prefix_length': 24}, {'prefix_length': 24}, {'prefix_length': 25}, ]) ``` ## IP Ranges ### Available IPs The `available_ips` property on an IP range works the same way as on a prefix, but allocates from the range's address pool. ::: pynetbox.models.ipam.IpRanges.available_ips handler: python options: show_source: true **Examples:** ```python ip_range = nb.ipam.ip_ranges.get(1) # List available IPs in the range available = ip_range.available_ips.list() # Allocate a single IP new_ip = ip_range.available_ips.create() # Allocate multiple IPs new_ips = ip_range.available_ips.create([{} for _ in range(10)]) # Allocate an IP with attributes new_ip = ip_range.available_ips.create({ 'description': 'DHCP reservation', 'status': 'reserved', }) ``` ## VLAN Groups ### Available VLANs The `available_vlans` property lists VLAN IDs that are not yet in use within a VLAN group, and allocates new VLANs from that pool. ::: pynetbox.models.ipam.VlanGroups.available_vlans handler: python options: show_source: true **Examples:** ```python vlan_group = nb.ipam.vlan_groups.get(name='Production') # Available VLAN IDs in the group available = vlan_group.available_vlans.list() # [10, 11, 12, 13, ...] # Create a new VLAN; NetBox picks the lowest available VID new_vlan = vlan_group.available_vlans.create({ 'name': 'NewVLAN', 'status': 'active', }) # Create a VLAN with a specific VID (must be available within the group) new_vlan = vlan_group.available_vlans.create({ 'name': 'Servers', 'vid': 100, 'status': 'active', }) ``` ## ASN Ranges ### Available ASNs The `available_asns` property lists ASNs that are not yet allocated within an ASN range, and allocates new ASNs from that pool. ::: pynetbox.models.ipam.AsnRanges.available_asns handler: python options: show_source: true **Examples:** ```python asn_range = nb.ipam.asn_ranges.get(name='Private ASN Pool') # Available ASNs available = asn_range.available_asns.list() # [64512, 64513, 64514, ...] # Allocate a single ASN new_asn = asn_range.available_asns.create() # Allocate multiple ASNs new_asns = asn_range.available_asns.create([{} for _ in range(5)]) ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/release-notes.md0000644000175100017510000001703615215017101017064 0ustar00runnerrunner# Release Notes ## Version 7.8.0 (June 18, 2026) #### Plugin Extension Framework ([#774](https://github.com/netbox-community/pynetbox/issues/774)) pynetbox can now be extended to support third-party NetBox plugins without modifying the library. You can register custom record types for a plugin's endpoints, so working with plugin objects feels the same as working with built-in NetBox objects. The new branching ([#710](https://github.com/netbox-community/pynetbox/issues/710)) and Custom Objects ([#751](https://github.com/netbox-community/pynetbox/issues/751)) integrations are built on this framework. #### Custom Thread Pool Executor ([#633](https://github.com/netbox-community/pynetbox/issues/633)) When threading is enabled, you can now control how the parallel requests behind `.all()` and `.filter()` are run. You can adjust how many worker threads are used, and supply your own executor — useful for carrying context such as tracing information into the worker threads. #### Cursor-Based Pagination ([#764](https://github.com/netbox-community/pynetbox/issues/764)) pynetbox can now page through large result sets using NetBox 4.6's cursor-based pagination. #### Other New Features - [#713](https://github.com/netbox-community/pynetbox/issues/713) - Add `sync()` method to the `data_sources` endpoint - [#715](https://github.com/netbox-community/pynetbox/issues/715) - Allow endpoints with underscores #### Enhancements - [#625](https://github.com/netbox-community/pynetbox/issues/625) - Preserve JSON list fields instead of returning them as `Record`s #### Bug Fixes - [#701](https://github.com/netbox-community/pynetbox/issues/701) - Fix filtering for custom fields - [#749](https://github.com/netbox-community/pynetbox/issues/749) - Don't treat `Record` methods as constructors in `_parse_values` - [#745](https://github.com/netbox-community/pynetbox/issues/745) - Serialize list-`Record` items that lack an ID - [#519](https://github.com/netbox-community/pynetbox/issues/519) - Fix link peer casting - [#688](https://github.com/netbox-community/pynetbox/issues/688) - Skip network fetch for dunder attributes in `Record.__getattr__` - [#748](https://github.com/netbox-community/pynetbox/issues/748) - Stop re-sending PATCH for unchanged partial `custom_fields` #### Compatibility - Supports NetBox 4.6 --- ## Version 7.7.0 (May 5, 2026) #### New Features - [#762](https://github.com/netbox-community/pynetbox/issues/762) - Support NetBox 4.6: add `VirtualMachineTypes` model, `virtual_machine_type` and `device` typed attributes on `VirtualMachines`, `oob_ip` typed attribute on `Devices`, and `dcim.cablebundle` / `dcim.rackgroup` content type mappings #### Bug Fixes - [#756](https://github.com/netbox-community/pynetbox/issues/756) - Use v2 token auth value in `create_token` (NetBox 4.5+) - [#708](https://github.com/netbox-community/pynetbox/issues/708) - `Record.serialize()` now includes new properties added by the user #### Compatibility - Supports NetBox 4.6 --- ## Version 7.6.1 (January 28, 2026) #### Enhancements - [#726](https://github.com/netbox-community/pynetbox/issues/726) - Use `dict` instead of `OrderedDict` in Record serialization #### New Features - [#434](https://github.com/netbox-community/pynetbox/issues/434) - Add cable path tracing support for front ports, rear ports, and virtual circuit terminations #### Bug Fixes - [#586](https://github.com/netbox-community/pynetbox/issues/586) - Update internal object state after save operations to prevent attribute reset issues --- ## Version 7.6.0 (January 9, 2026) #### Breaking Changes - **ObjectChange** moved to `core` module for NetBox 4.1.0+ compatibility - Previously located in `extras.object_changes` - Now accessible via `nb.core.object_changes` #### New Features - Support for v2 Tokens introduced in NetBox 4.5.0 - Enhanced token management capabilities #### Enhancements - Added SVG support for Rack Elevation endpoint - Rack elevation diagrams can now be rendered as SVG - Access via `rack.elevation.list(render='svg')` #### Bug Fixes - Fixed token authentication when retrieving NetBox version - Prevents 403 errors during version checks - Token now properly included in version API requests #### Compatibility - Supports NetBox 4.5 --- ## Version 7.5.0 (May 20, 2024) #### Enhancements - Expanded cable trace functionality to include: - CircuitTerminations - ConsolePorts - ConsoleServerPorts - PowerOutlets - PowerPorts - Added built-in function to activate a branch - Use `nb.activate_branch()` context manager for NetBox branching plugin #### Bug Fixes - Fixed choices returned when API tokens have PUT but not POST permissions - Fixed `nb.version` property when using OIDC proxy authentication #### Compatibility - Supports NetBox 4.1, 4.2, 4.3, 4.4 --- ## Version 7.4.1 (October 25, 2024) #### Security - Updated `requests` and `urllib3` Python libraries to address security vulnerabilities - No functional changes --- ## Version 7.4.0 (August 8, 2024) #### Enhancements - Added initial NetBox 4.0 support - Added Python 3.12 support #### Bug Fixes - Fixed complex custom_fields insertion failures - Replaced `None` with `'null'` in query parameters - Corrected connected endpoints behavior #### Testing - Removed Python 3.8 and 3.9 from test matrix - Added Python 3.12 to CI/CD pipeline #### Compatibility - Supports NetBox 4.0.6+ --- ## Version 7.3.4 (July 2, 2024) #### Bug Fixes - Fixed API version detection for NetBox versions exceeding 4.x - Removed linting errors #### Testing - Dropped NetBox 3.3 from test matrix - Focus on NetBox 4.x support --- ## Version 7.3.3 (January 5, 2024) #### Fixes - PyPI release fix - No functional changes --- ## Version 7.3.2 (January 4, 2024) #### Fixes - Fixed setup.py for new publish workflow - No functional changes --- ## Version 7.3.1 (January 4, 2024) #### Fixes - Updated PyPI publish workflow - No functional changes --- ## Version 7.3.0 (January 3, 2024) #### New Features - Added NetBox v3.7 support #### Dependencies - Added `pyyaml` dependency #### Testing - Updated test suite for NetBox 3.7 compatibility #### Compatibility - Supports NetBox 3.7 --- ## Version 7.2.0 (September 7, 2023) #### New Features - Added NetBox v3.6 support #### Compatibility - Supports NetBox 3.6 --- ## Version 7.1.0 (August 2023) #### Code Quality - Lint fixes and code cleanup - Improved code formatting consistency #### Compatibility - Supports NetBox 3.5 --- ## Version 7.0.1 (June 2023) #### Enhancements - Updated code formatting - Documentation improvements --- ## Version 7.0.0 (June 2023) #### Breaking Changes - **Minimum NetBox version**: 3.3+ - Removed support for NetBox versions below 3.3 #### New Features - Full NetBox 3.3 API support - Updated test suite for NetBox 3.3 #### Testing - Completely overhauled test scripts - Updated test settings for NetBox 3.3 compatibility - Improved integration test coverage #### Compatibility - **NetBox 3.3+ required** - Python 3.8+ required --- ## Version 6.7.x and Earlier For release notes for versions 6.7 and earlier, please refer to the [GitHub Releases page](https://github.com/netbox-community/pynetbox/releases). Key highlights from earlier versions: #### Version 6.7 (Last pre-7.0 release) - Last version to support NetBox < 3.3 - Python 3.7+ support #### Version 6.6 - Custom error pickling fixes - VLAN __str__ improvements - VirtualChassis enhancements #### Version 6.5 - Added available-vlans support for VLAN groups #### Version 6.4 - Added Wireless app support - Data returned as dict improvements #### Version 6.3 - Field name lookup improvements - Enhanced filtering capabilities ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/response.md0000644000175100017510000000243015215017101016144 0ustar00runnerrunner# Response This page documents the classes that wrap responses returned by NetBox: `Record` (a single object) and `RecordSet` (a lazy collection of records). ## Record Class A `Record` represents a single object returned by the NetBox API. API fields are exposed as attributes; nested objects are recursively wrapped as their own `Record` instances. Records returned from a list endpoint are initially "shallow" — accessing an attribute not present in the list response causes pynetbox to fetch the full detail view on demand. ::: pynetbox.core.response.Record handler: python options: members: - delete - full_details - save - serialize - update - updates show_source: true show_root_heading: true heading_level: 3 ## RecordSet Class A `RecordSet` is a one-shot iterator over `Record` objects, returned by `Endpoint.all()` and `Endpoint.filter()`. It pages through results from NetBox on demand. To iterate the results more than once, materialize the set with `list()`. ::: pynetbox.core.response.RecordSet handler: python options: members: - delete - update show_source: true show_root_heading: true heading_level: 3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/docs/virtualization.md0000644000175100017510000000170315215017101017374 0ustar00runnerrunner# Virtualization This page documents the special methods available on Virtualization models. Standard CRUD operations follow the patterns described in [Quick Start](getting-started.md) and the [Endpoint reference](endpoint.md). !!! note "Standard API Operations" The standard endpoint methods (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for the available fields and filters on each endpoint. ## Virtual Machines ### Config Rendering The `render_config` property renders a virtual machine's configuration from its assigned config template and config contexts. ::: pynetbox.models.virtualization.VirtualMachines.render_config handler: python options: show_source: true **Example:** ```python vm = nb.virtualization.virtual_machines.get(name='web-vm-01') config = vm.render_config.create() print(config) ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/mkdocs.yml0000644000175100017510000000372615215017101015050 0ustar00runnerrunnersite_name: pynetbox site_description: Python API client library for NetBox repo_name: netbox-community/pynetbox repo_url: https://github.com/netbox-community/pynetbox theme: name: material features: - content.code.copy - navigation.footer palette: - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/lightbulb-outline name: Switch to Dark Mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/lightbulb name: Switch to Light Mode plugins: - search: lang: en - mkdocstrings: default_handler: python handlers: python: options: show_source: true show_root_heading: true heading_level: 3 markdown_extensions: - admonition - attr_list - footnotes - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true nav: - Home: index.md - Getting Started: - Installation: installation.md - Quick Start: getting-started.md - API Reference: - Core Classes: api.md - Endpoint: endpoint.md - Response: response.md - Exceptions: exceptions.md - Circuits: circuits.md - Core: core.md - DCIM: dcim.md - IPAM: ipam.md - Virtualization: virtualization.md - Advanced Topics: - Advanced Usage: advanced.md - Extensions: extensions.md - Branching: branching.md - Custom Objects: custom-objects.md - Development: - Development Guide: development/index.md - Getting Started: development/getting-started.md - Release Checklist: development/release-checklist.md - Release Notes: release-notes.md ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9468527 pynetbox-7.8.0/pynetbox/0000755000175100017510000000000015215017112014707 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/__init__.py0000644000175100017510000000100015215017101017005 0ustar00runnerrunnerfrom pynetbox.core.api import Api from pynetbox.core.extension import Extension from pynetbox.core.query import ( AllocationError, ContentError, RequestError, ParameterValidationError, ) from pynetbox.core.response import JsonField __version__ = "7.8.0" # Lowercase alias for backward compatibility api = Api __all__ = ( "Api", "AllocationError", "ContentError", "Extension", "JsonField", "RequestError", "ParameterValidationError", "api", "__version__", ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9484007 pynetbox-7.8.0/pynetbox/core/0000755000175100017510000000000015215017112015637 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/__init__.py0000644000175100017510000000000015215017101017734 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/api.py0000644000175100017510000004064215215017101016766 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import contextlib import warnings import requests from packaging import version from packaging.version import InvalidVersion from pynetbox.core.app import App, PluginsApp from pynetbox.core.query import Request, RequestError, TOKEN_PREFIX from pynetbox.core.response import Record from pynetbox.models.mapper import CONTENT_TYPE_MAPPER class Api: """The API object is the point of entry to pynetbox. After instantiating the Api() with the appropriate named arguments you can specify which app and endpoint you wish to interact with. Valid attributes currently are: * circuits * core (NetBox 3.5+) * dcim * extras * ipam * tenancy * users * virtualization * vpn (NetBox 3.7+) * wireless Calling any of these attributes will return an `App` object which exposes endpoints as attributes. ## Additional Attributes * **http_session(requests.Session)**: Override the default session with your own. This is used to control a number of HTTP behaviors such as SSL verification, custom headers, retires, and timeouts. See [custom sessions](advanced.md#custom-sessions) for more info. ## Parameters * **url** (str): The base URL to the instance of NetBox you wish to connect to. * **token** (str): Your NetBox token. * **threading** (bool, optional): Set to True to use threading in `.all()` and `.filter()` requests. * **thread_pool_executor** (callable, optional): A `concurrent.futures.ThreadPoolExecutor` class (or any callable matching its `(max_workers=...)` signature and context-manager protocol) used to build the pool for threaded requests. Defaults to `concurrent.futures.ThreadPoolExecutor`. Inject a custom executor to propagate thread-local state such as OpenTelemetry trace context into worker threads. * **max_workers** (int, optional): Maximum number of worker threads used for threaded `.all()` and `.filter()` requests. Defaults to 4. Only used when `threading=True`. ## Raises * **AttributeError**: If app doesn't exist. ## Examples ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) list(nb.dcim.devices.all()) # [test1-leaf1, test1-leaf2, test1-leaf3] ``` """ def __init__( self, url, token=None, threading=False, strict_filters=False, extensions=None, pagination="offset", thread_pool_executor=None, max_workers=4, ): """Initialize the API client. Args: url (str): The base URL to the instance of NetBox you wish to connect to. token (str, optional): Your NetBox API token. If not provided, authentication will be required for each request. threading (bool, optional): Set to True to use threading in `.all()` and `.filter()` requests, defaults to False. strict_filters (bool, optional): Set to True to check GET call filters against OpenAPI specifications (intentionally not done in NetBox API), defaults to False. extensions (list, optional): A list of `Extension` classes or instances that register custom `Record` subclasses and content-type mappings for NetBox plugins. See `pynetbox.core.extension`. pagination (str, optional): Pagination strategy for `.all()` and `.filter()`, either `"offset"` (default) or `"cursor"`. Cursor pagination (NetBox 4.6+) offers better performance on very large result sets but omits the total count and cannot be combined with threading or `ordering`. On NetBox versions older than 4.6 it transparently falls back to offset pagination. thread_pool_executor (callable, optional): A `concurrent.futures.ThreadPoolExecutor` class, or any callable matching its `(max_workers=...)` signature and context-manager protocol, used to build the pool for threaded requests. Defaults to `concurrent.futures.ThreadPoolExecutor`. max_workers (int, optional): Maximum number of worker threads used for threaded requests, defaults to 4. """ if pagination not in ("offset", "cursor"): raise ValueError( "pagination must be 'offset' or 'cursor', got {!r}".format(pagination) ) if max_workers <= 0: raise ValueError("max_workers must be a positive integer") base_url = "{}/api".format(url if url[-1] != "/" else url[:-1]) self.token = token self.base_url = base_url self.http_session = requests.Session() self.threading = threading # Stored as ``None`` (the sentinel) when the caller did not supply a # custom executor, so ``None`` distinguishes "use the default" from an # explicit choice. ``Request`` resolves ``None`` to # ``concurrent.futures.ThreadPoolExecutor`` at request time, so the # value here is not necessarily the executor actually used. self.thread_pool_executor = thread_pool_executor self.max_workers = max_workers self.strict_filters = strict_filters self.pagination = pagination self._cursor_supported = None self._register_extensions(extensions or []) # Initialize NetBox apps self.circuits = App(self, "circuits") self.core = App(self, "core") self.dcim = App(self, "dcim") self.extras = App(self, "extras") self.ipam = App(self, "ipam") self.tenancy = App(self, "tenancy") self.users = App(self, "users") self.virtualization = App(self, "virtualization") self.vpn = App(self, "vpn") self.wireless = App(self, "wireless") self.plugins = PluginsApp(self) def _register_extensions(self, extensions): """Build per-instance extension and content-type registries. Stores extensions keyed by ``plugin_name`` so ``PluginsApp`` and ``App._setmodel`` can look up the right ``models`` namespace, and composes the built-in ``CONTENT_TYPE_MAPPER`` with each extension's ``content_types`` overrides. Two extensions cannot share a ``plugin_name`` or register the same content-type key — both raise ``ValueError``, since a silent last-wins would mask a configuration mistake. Overriding a key from the built-in ``CONTENT_TYPE_MAPPER`` is allowed and intentional. ``plugin_name`` is normalized by converting dashes to underscores before lookup so the same plugin cannot register itself twice under both spellings (e.g. ``"custom-objects"`` and ``"custom_objects"``). """ self._extensions = {} for ext in extensions: plugin_name = getattr(ext, "plugin_name", None) if not plugin_name: raise ValueError( "Extension {!r} is missing a 'plugin_name' attribute".format(ext) ) plugin_name = plugin_name.replace("-", "_") if plugin_name in self._extensions: raise ValueError( "Duplicate extension for plugin_name {!r}: {!r} and {!r}".format( plugin_name, self._extensions[plugin_name], ext ) ) self._extensions[plugin_name] = ext content_types = dict(CONTENT_TYPE_MAPPER) seen_extension_keys = {} for ext in self._extensions.values(): ext_content_types = getattr(ext, "content_types", None) or {} for key, value in ext_content_types.items(): if key in seen_extension_keys: raise ValueError( "Duplicate content_type {!r} registered by extensions " "{!r} and {!r}".format(key, seen_extension_keys[key], ext) ) seen_extension_keys[key] = ext content_types[key] = value self._content_type_mapper = content_types self._type_content_mapper = { v: k for k, v in content_types.items() if v is not None } @property def version(self): """Gets the API version of NetBox. Can be used to check the NetBox API version if there are version-dependent features or syntaxes in the API. ## Returns Version number as a string. ## Example ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) nb.version # '3.1' ``` """ version = Request( base=self.base_url, token=self.token, http_session=self.http_session, ).get_version() return version def _effective_pagination(self): """Resolve the pagination strategy to use for list requests. Returns ``"cursor"`` only when cursor pagination was requested *and* the connected NetBox supports it (4.6+). Otherwise returns ``"offset"``. The server version is probed once and cached, so only the first `.all()`/`.filter()` on a cursor-mode `Api` pays the cost; offset-mode instances never make the extra request. """ if self.pagination != "cursor": return "offset" if self._cursor_supported is None: try: self._cursor_supported = version.parse(self.version) >= version.parse( "4.6" ) except (RequestError, InvalidVersion, requests.exceptions.RequestException): # RequestError covers a non-ok HTTP response from the version # probe; requests.exceptions.RequestException covers transport # failures (ConnectionError, Timeout, ...) raised before a # response exists. In every case fall back to offset, as the # docstring promises, and let the real list request surface any # underlying connectivity error. self._cursor_supported = False if self._cursor_supported and self.threading: # Cursor pagination follows next links sequentially and cannot # be parallelised; the cursor path ignores self.threading. # Warn so the no-op threading configuration is not a silent # performance surprise. # stacklevel=5 attributes the warning to the caller's list # request rather than to pynetbox internals. The version probe # is resolved lazily on the first page fetch, so the fixed # frame chain at this point is: # _effective_pagination -> Request._resolve_pagination # -> Request.get -> RecordSet.__next__/__len__ -> caller. warnings.warn( "threading=True has no effect with cursor pagination; " "cursor pages are fetched sequentially.", stacklevel=5, ) return "cursor" if self._cursor_supported else "offset" def openapi(self): """Returns the OpenAPI spec. Quick helper function to pull down the entire OpenAPI spec. It is stored in memory to avoid repeated calls on NetBox API. ## Returns dict: The OpenAPI specification as a dictionary. ## Example ```python import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) nb.openapi() # {...} ``` """ if not (openapi := getattr(self, "_openapi", None)): openapi = self._openapi = Request( base=self.base_url, http_session=self.http_session, ).get_openapi() return openapi def status(self): """Gets the status information from NetBox. ## Returns Dictionary containing NetBox status information. ## Raises `RequestError`: If the request is not successful. ## Example ```python from pprint import pprint pprint(nb.status()) { 'django-version': '3.1.3', 'installed-apps': { 'cacheops': '5.0.1', 'debug_toolbar': '3.1.1', 'django_filters': '2.4.0', 'django_prometheus': '2.1.0', 'django_rq': '2.4.0', 'django_tables2': '2.3.3', 'drf_yasg': '1.20.0', 'mptt': '0.11.0', 'rest_framework': '3.12.2', 'taggit': '1.3.0', 'timezone_field': '4.0' }, 'netbox-version': '2.10.2', 'plugins': {}, 'python-version': '3.7.3', 'rq-workers-running': 1 } ``` """ status = Request( base=self.base_url, token=self.token, http_session=self.http_session, ).get_status() return status def create_token(self, username, password): """Creates an API token using a valid NetBox username and password. Saves the created token automatically in the API object. ## Parameters * **username** (str): NetBox username * **password** (str): NetBox password ## Returns `Record`: The token as a Record object. ## Raises `RequestError`: If the request is not successful. ## Notes NetBox 4.5 introduced v2 tokens. For v2 tokens, `nb.token` is set to `nbt_.` (the full auth value required in the Authorization header), which differs from `token.key`. For v1 tokens (pre-4.5), `nb.token` is the plaintext token value. ## Example ```python import pynetbox nb = pynetbox.api("https://netbox-server") token = nb.create_token("admin", "netboxpassword") # NetBox 4.5+ v2 token: nb.token differs from token.key nb.token # 'nbt_shortkey1234567.plaintexttoken7890abcdef1234567890abcdef' token.key # 'shortkey1234567' # Pre-4.5 / v1 token: nb.token matches token.key (or token.token) nb.token # '96d02e13e3f1fdcd8b4c089094c0191dcb045bef' ``` """ resp = Request( base="{}/users/tokens/provision/".format(self.base_url), http_session=self.http_session, ).post(data={"username": username, "password": password}) # v2 tokens (NetBox 4.5+): construct auth value as nbt_. if resp.get("version") == 2: self.token = "{}{}.{}".format(TOKEN_PREFIX, resp["key"], resp["token"]) else: self.token = resp.get("token") or resp["key"] return Record(resp, self, None) @contextlib.contextmanager def activate_branch(self, branch): """Context manager to activate the branch by setting the schema ID in the headers. **Note**: The NetBox branching plugin must be installed and enabled in your NetBox instance for this functionality to work. ## Parameters * **branch** (Record): The NetBox branch to activate ## Raises `ValueError`: If the branch is not a valid NetBox branch. ## Example ```python import pynetbox nb = pynetbox.api("https://netbox-server") branch = nb.plugins.branching.branches.create(name="testbranch") with nb.activate_branch(branch): sites = nb.dcim.sites.all() # All operations within this block will use the branch's schema ``` """ if not isinstance(branch, Record) or "schema_id" not in dict(branch): raise ValueError( f"The specified branch is not a valid NetBox branch: {branch}." ) self.http_session.headers["X-NetBox-Branch"] = branch.schema_id try: yield finally: self.http_session.headers.pop("X-NetBox-Branch", None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/app.py0000644000175100017510000001244015215017101016770 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.endpoint import Endpoint from pynetbox.core.query import Request from pynetbox.models import ( circuits, core, dcim, extras, ipam, users, virtualization, wireless, ) class App: """Represents apps in NetBox. Calls to attributes are returned as Endpoint objects. ## Returns Endpoint matching requested attribute. ## Raises RequestError if requested endpoint doesn't exist. """ def __init__(self, api, name): self.api = api self.name = name self._setmodel() models = { "circuits": circuits, "core": core, "dcim": dcim, "extras": extras, "ipam": ipam, "users": users, "virtualization": virtualization, "wireless": wireless, } _PLUGINS_PREFIX = "plugins/" def _setmodel(self): if self.name.startswith(self._PLUGINS_PREFIX): # Plugin apps may carry a custom models namespace registered # via an Extension. The plugin attribute uses underscores while # the URL slug uses dashes (PluginsApp.__getattr__), so convert # back when looking up the registry. plugin_slug = self.name[len(self._PLUGINS_PREFIX) :] plugin_name = plugin_slug.replace("-", "_") extensions = getattr(self.api, "_extensions", {}) ext = extensions.get(plugin_name) self.model = getattr(ext, "models", None) if ext is not None else None else: self.model = App.models.get(self.name) def __getstate__(self): return {"api": self.api, "name": self.name} def __setstate__(self, d): self.__dict__.update(d) self._setmodel() def __getattr__(self, name): return Endpoint(self.api, self, name, model=self.model) def endpoint(self, name): """Return an Endpoint using ``name`` as the literal URL slug. Attribute access (``app.ip_addresses``) converts underscores to dashes, which is correct for the vast majority of NetBox endpoints. This method skips that conversion so endpoints whose slug genuinely contains underscores can be reached. ## Parameters * **name** (str): The endpoint slug, used verbatim (no ``_`` → ``-``). ## Returns Endpoint matching the given slug. ## Examples ```python nb.plugins.custom_objects.endpoint("my_custom_object").all() ``` """ return Endpoint(self.api, self, name, model=self.model, literal_name=True) def config(self): """Returns config response from app. ## Returns Raw response from NetBox's config endpoint. ## Raises RequestError if called for an invalid endpoint. ## Examples ```python pprint.pprint(nb.users.config()) { 'tables': { 'DeviceTable': { 'columns': [ 'name', 'status', 'tenant', 'role', 'site', 'primary_ip', 'tags' ] } } } ``` """ config = Request( base="{}/{}/config/".format( self.api.base_url, self.name, ), token=self.api.token, http_session=self.api.http_session, ).get() return config class PluginsApp: """Basically valid plugins api could be handled by same App class, but you need to add plugins to request url path. ## Returns App with added plugins into path. """ def __init__(self, api): self.api = api def __getstate__(self): return self.__dict__ def __setstate__(self, d): self.__dict__.update(d) def __getattr__(self, name): return App(self.api, "plugins/{}".format(name.replace("_", "-"))) def installed_plugins(self): """Returns raw response with installed plugins. ## Returns Raw response NetBox's installed plugins. ## Examples ```python nb.plugins.installed_plugins() [ { 'name': 'test_plugin', 'package': 'test_plugin', 'author': 'Dmitry', 'description': 'Netbox test plugin', 'verison': '0.10' } ] ``` """ installed_plugins = Request( base="{}/plugins/installed-plugins".format( self.api.base_url, ), token=self.api.token, http_session=self.api.http_session, ).get() return installed_plugins ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/endpoint.py0000644000175100017510000006002315215017101020030 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.query import Request, RequestError, ParameterValidationError from pynetbox.core.response import Record, RecordSet RESERVED_KWARGS = () class Endpoint: """Represent actions available on endpoints in the Netbox API. Takes ``name`` and ``app`` passed from App() and builds the correct url to make queries to and the proper Response object to return results in. ## Parameters * **api** (Api): Takes Api created at instantiation. * **app** (App): Takes App. * **name** (str): Name of endpoint passed to App(). * **model** (obj, optional): Custom model for given app. * **literal_name** (bool, optional): When True, use ``name`` verbatim as the URL slug instead of converting underscores to dashes. Used to reach endpoints whose slug legitimately contains underscores (e.g. custom objects). Defaults to False. ## Note In order to call NetBox endpoints with dashes in their names you should convert the dash to an underscore. (E.g. querying the ip-addresses endpoint is done with ``nb.ipam.ip_addresses.all()``.) For the rare endpoint whose slug really does contain underscores, use ``App.endpoint()`` (e.g. ``nb.plugins.custom_objects.endpoint("my_custom_object")``) which sets ``literal_name=True`` so no conversion is applied. """ def __init__(self, api, app, name, model=None, literal_name=False): self.return_obj = self._lookup_ret_obj(name, model) self.name = name if literal_name else name.replace("_", "-") self.api = api self.app = app self.base_url = api.base_url self.token = api.token self.url = "{base_url}/{app}/{endpoint}".format( base_url=self.base_url, app=app.name, endpoint=self.name, ) self._choices = None def _lookup_ret_obj(self, name, model): """Loads unique Response objects. This method loads a unique response object for an endpoint if it exists. Otherwise return a generic `Record` object. ## Parameters * **name** (str): Endpoint name. * **model** (obj): The application model that contains unique Record objects. ## Returns Record (obj) """ if model: name = name.title().replace("_", "") ret = getattr(model, name, Record) else: ret = Record return ret def _validate_openapi_parameters(self, method: str, parameters: dict) -> None: """Validate GET request parameters against OpenAPI specification This method raises a **ParameterValidationError** if parameters passed to NetBox API do not match the OpenAPI specification or validation fails. ## Parameters * **method** : Only "get" is supported as for other methods NetBox already does proper validation * **parameters** : kwargs passed to filter() method ## Returns None """ if method.lower() != "get": raise RuntimeError(f"Unsupported method '{method}'.") openapi_definition_path = "/api/{app}/{endpoint}/".format( app=self.app.name, endpoint=self.name, ) # Parse NetBox OpenAPI definition try: openapi_definition = self.api.openapi()["paths"].get( openapi_definition_path ) if not openapi_definition: raise ParameterValidationError( f"Path '{openapi_definition_path}' does not exist in NetBox OpenAPI specification." ) openapi_parameters = openapi_definition[method]["parameters"] allowed_parameters = [p["name"] for p in openapi_parameters] except KeyError as exc: raise ParameterValidationError( f"Error while parsing Netbox OpenAPI specification: {exc}" ) # Validate all parameters. Custom field filters (cf_, plus # lookup expressions like cf___gt) are per-instance dynamic # and not listed in the OpenAPI spec, so they are skipped here. validation_errors = [] for p in parameters: if p.startswith("cf_"): continue if p not in allowed_parameters: validation_errors.append( f"'{p}' is not allowed as parameter on path '{openapi_definition_path}'." ) if len(validation_errors) > 0: raise ParameterValidationError(validation_errors) def all(self, limit=0, offset=None): """Queries the 'ListView' of a given endpoint. Returns all objects from an endpoint. ## Parameters * **limit** (int, optional): Overrides the max page size on paginated returns. This defines the number of records that will be returned with each query to the Netbox server. The queries will be made as you iterate through the result set. * **offset** (int, optional): Overrides the offset on paginated returns. ## Returns A RecordSet object. ## Examples ```python devices = list(nb.dcim.devices.all()) for device in devices: print(device.name) # test1-leaf1 # test1-leaf2 # test1-leaf3 ``` If you want to iterate over the results multiple times then encapsulate them in a list like this: ```python devices = list(nb.dcim.devices.all()) ``` This will cause the entire result set to be fetched from the server. """ if limit == 0 and offset is not None: raise ValueError("offset requires a positive limit value") req = Request( base="{}/".format(self.url), token=self.token, http_session=self.api.http_session, threading=self.api.threading, thread_pool_executor=self.api.thread_pool_executor, max_workers=self.api.max_workers, limit=limit, offset=offset, # Passed uncalled so the version probe behind # _effective_pagination() is deferred until the request actually # runs, rather than firing when this lazy RecordSet is built. pagination=self.api._effective_pagination, ) return RecordSet(self, req) def get(self, *args, **kwargs): """Queries the DetailsView of a given endpoint. ## Parameters * **key** (int, optional): id for the item to be retrieved. * **kwargs**: Accepts the same keyword args as filter(). Any search argument the endpoint accepts can be added as a keyword arg. * **strict_filters** (bool, optional): Overrides the global filter validation per-request basis. Handled by the filter() method. ## Returns A single Record object or None ## Raises ValueError: if kwarg search return more than one value. ## Examples Referencing with a kwarg that only returns one value: ```python nb.dcim.devices.get(name='test1-a3-tor1b') # test1-a3-tor1b ``` Referencing with an id: ```python nb.dcim.devices.get(1) # test1-edge1 ``` Using multiple named arguments. For example, retrieving the location when the location name is not unique and used in multiple sites: ```python nb.locations.get(site='site-1', name='Row 1') # Row 1 ``` """ try: key = args[0] except IndexError: key = None if not key: resp = self.filter(**kwargs) ret = next(resp, None) if not ret: return ret try: next(resp) raise ValueError( "get() returned more than one result. " "Check that the kwarg(s) passed are valid for this " "endpoint or use filter() or all() instead." ) except StopIteration: return ret req = Request( key=key, base=self.url, token=self.token, http_session=self.api.http_session, ) try: return next(RecordSet(self, req), None) except RequestError as e: if e.req.status_code == 404: return None else: raise e def filter(self, *args, **kwargs): """Queries the 'ListView' of a given endpoint. Takes named arguments that match the usable filters on a given endpoint. If an argument is passed then it's used as a freeform search argument if the endpoint supports it. ## Parameters * **args** (str, optional): Freeform search string that's accepted on given endpoint. * **kwargs** (str, optional): Any search argument the endpoint accepts can be added as a keyword arg. * **limit** (int, optional): Overrides the max page size on paginated returns. This defines the number of records that will be returned with each query to the Netbox server. The queries will be made as you iterate through the result set. * **offset** (int, optional): Overrides the offset on paginated returns. * **strict_filters** (bool, optional): Overrides the global filter validation per-request basis. ## Returns A RecordSet object. ## Examples To return a list of objects matching a named argument filter: ```python devices = nb.dcim.devices.filter(role='leaf-switch') for device in devices: print(device.name) # test1-leaf1 # test1-leaf2 # test1-leaf3 ``` ```python devices = nb.dcim.devices.filter(site='site-1') for device in devices: print(device.name) # test1-a2-leaf1 # test2-a2-leaf2 ``` ## Note If a keyword argument is incorrect a `TypeError` will not be returned by pynetbox. Instead, pynetbox will return all records filtered up to the last correct keyword argument. For example, if we used `site="Site 1"` instead of `site=site-1` when using filter on the devices endpoint, then pynetbox will return **all** devices across all sites instead of devices at Site 1. Using a freeform query along with a named argument: ```python devices = nb.dcim.devices.filter('a3', role='leaf-switch') for device in devices: print(device.name) # test1-a3-leaf1 # test1-a3-leaf2 ``` """ if args: kwargs.update({"q": args[0]}) if any(i in RESERVED_KWARGS for i in kwargs): raise ValueError( "A reserved kwarg was passed ({}). Please remove it " "and try again.".format(RESERVED_KWARGS) ) limit = kwargs.pop("limit") if "limit" in kwargs else 0 offset = kwargs.pop("offset") if "offset" in kwargs else None strict_filters = ( # kwargs value takes precedence on globally set value kwargs.pop("strict_filters") if "strict_filters" in kwargs else self.api.strict_filters ) if limit == 0 and offset is not None: raise ValueError("offset requires a positive limit value") filters = {x: y if y is not None else "null" for x, y in kwargs.items()} if strict_filters: self._validate_openapi_parameters("get", filters) req = Request( filters=filters, base=self.url, token=self.token, http_session=self.api.http_session, threading=self.api.threading, thread_pool_executor=self.api.thread_pool_executor, max_workers=self.api.max_workers, limit=limit, offset=offset, # Passed uncalled so the version probe behind # _effective_pagination() is deferred until the request actually # runs, rather than firing when this lazy RecordSet is built. pagination=self.api._effective_pagination, ) return RecordSet(self, req) def create(self, *args, **kwargs): """Creates an object on an endpoint. Takes named arguments that match the given endpoint's available fields. Returns a new object. ## Parameters * **args**: Not used. * **kwargs**: Fields and values to create the object with. ## Returns A Record object. ## Examples Creating a new device: ```python new_device = nb.dcim.devices.create( name='test-device', device_type=1, device_role=1, site=1 ) ``` Creating a new device with a nested object: ```python new_device = nb.dcim.devices.create( name='test-device', device_type={'id': 1}, device_role={'id': 1}, site={'id': 1} ) ``` """ req = Request( base=self.url, token=self.token, http_session=self.api.http_session, ).post(args[0] if args else kwargs) if isinstance(req, list): return [self.return_obj(i, self.api, self) for i in req] return self.return_obj(req, self.api, self) def update(self, objects): """Updates objects in NetBox. Takes a list of objects and updates them in NetBox. ## Parameters * **objects** (list): A list of Record objects to update. ## Returns A list of Record objects. ## Examples ```python devices = nb.dcim.devices.filter(site='test1') for device in devices: device.status = 'active' nb.dcim.devices.update(devices) ``` """ series = [] if not isinstance(objects, list): raise ValueError( "Objects passed must be list[dict|Record] - was {}".format( type(objects) ) ) for o in objects: if isinstance(o, Record): data = o.updates() if data: data["id"] = o.id series.append(data) elif isinstance(o, dict): if "id" not in o: raise ValueError("id is missing from object: " + str(o)) series.append(o) else: raise ValueError( "Object passed must be dict|Record - was {}".format(type(objects)) ) req = Request( base=self.url, token=self.token, http_session=self.api.http_session, ).patch(series) if isinstance(req, list): return [self.return_obj(i, self.api, self) for i in req] return self.return_obj(req, self.api, self) def delete(self, objects): """Deletes objects from NetBox. Takes a list of objects and deletes them from NetBox. ## Parameters * **objects** (list): A list of Record objects to delete. ## Returns True if the delete operation was successful. ## Examples ```python devices = nb.dcim.devices.filter(site='test1') nb.dcim.devices.delete(devices) ``` """ cleaned_ids = [] if not isinstance(objects, list) and not isinstance(objects, RecordSet): raise ValueError( "objects must be list[str|int|Record]" "|RecordSet - was " + str(type(objects)) ) for o in objects: if isinstance(o, int): cleaned_ids.append(o) elif isinstance(o, str) and o.isnumeric(): cleaned_ids.append(int(o)) elif isinstance(o, Record): if not hasattr(o, "id"): raise ValueError( "Record from '" + o.url + "' does not have an id and cannot be bulk deleted" ) cleaned_ids.append(o.id) else: raise ValueError( "Invalid object in list of objects to delete: " + str(type(o)) ) req = Request( base=self.url, token=self.token, http_session=self.api.http_session, ) return True if req.delete(data=[{"id": i} for i in cleaned_ids]) else False def choices(self): """Returns all choices from the endpoint if it has them. ## Returns Dictionary of available choices. ## Examples ```python choices = nb.dcim.devices.choices() print(choices['status']) { 'label': 'Active', 'value': 'active' } ``` """ if self._choices: return self._choices req = Request( base=self.url, token=self.api.token, http_session=self.api.http_session, ).options() actions = req.get("actions", {}) post_data = actions.get("POST") or actions.get("PUT") if post_data is None: raise ValueError( "Unexpected format in the OPTIONS response at {}".format(self.url) ) self._choices = {} for prop in post_data: if "choices" in post_data[prop]: self._choices[prop] = post_data[prop]["choices"] return self._choices def count(self, *args, **kwargs): """Returns the count of objects in a query. Takes named arguments that match the usable filters on a given endpoint. If an argument is passed then it's used as a freeform search argument if the endpoint supports it. ## Parameters * **args** (str, optional): Freeform search string that's accepted on given endpoint. * **kwargs** (str, optional): Any search argument the endpoint accepts can be added as a keyword arg. ## Returns Integer of count of objects. ## Examples ```python nb.dcim.devices.count(site='test1') # 27 ``` """ if args: kwargs.update({"q": args[0]}) if any(i in RESERVED_KWARGS for i in kwargs): raise ValueError( "A reserved {} kwarg was passed. Please remove it " "try again.".format(RESERVED_KWARGS) ) ret = Request( filters=kwargs, base=self.url, token=self.token, http_session=self.api.http_session, ) return ret.get_count() class DetailEndpoint: """Enables read/write operations on detail endpoints. Endpoints like `available-ips` that are detail routes off traditional endpoints are handled with this class. """ def __init__(self, parent_obj, name, custom_return=None): self.parent_obj = parent_obj self.custom_return = custom_return self.url = "{}/{}/{}/".format(parent_obj.endpoint.url, parent_obj.id, name) self.request_kwargs = dict( base=self.url, token=parent_obj.api.token, http_session=parent_obj.api.http_session, ) def list(self, **kwargs): """The view operation for a detail endpoint. Returns the response from NetBox for a detail endpoint. ## Parameters * **kwargs**: Key/value pairs that get converted into URL parameters when passed to the endpoint. E.g. `.list(method='get_facts')` would be converted to `.../?method=get_facts`. ## Returns A Record object or list of Record objects created from data retrieved from NetBox. """ req = Request(**self.request_kwargs).get(add_params=kwargs) if self.custom_return: return [ self.custom_return( i, self.parent_obj.endpoint.api, self.parent_obj.endpoint ) for i in req ] return req def create(self, data=None, **kwargs): """The write operation for a detail endpoint. Creates objects on a detail endpoint in NetBox. ## Parameters * **data** (dict/list, optional): A dictionary or list containing the key/value pair of the items you're creating on the parent object. Defaults to empty dict which will create a single item with default values. * **kwargs**: Alternative to `data`, fields and values to create the object with — mirrors `Endpoint.create()`. Cannot be combined with `data`. ## Returns A Record object or list of Record objects created from data created in NetBox. """ if data is not None and kwargs: raise ValueError("Cannot pass both data and keyword arguments") payload = data if data is not None else kwargs req = Request(**self.request_kwargs).post(payload) if self.custom_return: if isinstance(req, list): return [ self.custom_return( req_item, self.parent_obj.endpoint.api, self.parent_obj.endpoint ) for req_item in req ] else: return self.custom_return( req, self.parent_obj.endpoint.api, self.parent_obj.endpoint ) return req class RODetailEndpoint(DetailEndpoint): def create(self, data=None, **kwargs): raise NotImplementedError("Writes are not supported for this endpoint.") class ROMultiFormatDetailEndpoint(RODetailEndpoint): """Read-only detail endpoint supporting multiple response formats. Handles endpoints that return data in different formats based on query parameters. Supports both structured data (JSON) and raw formats (e.g., SVG). The endpoint inspects the 'render' parameter to determine response format: - No parameter or render='json': Returns structured JSON data - render='svg': Returns raw SVG content ## Examples ```python rack = nb.dcim.racks.get(123) rack.elevation.list() # Returns: list of rack unit objects rack.elevation.list(render='svg') # Returns: SVG string rack.elevation.list(render='json') # Returns: list of rack unit objects ``` """ def list(self, **kwargs): """Returns data in the requested format. ## Parameters * **kwargs**: Key/value pairs that get converted into URL parameters. Supports 'render' parameter for format selection. ## Returns - If render is non-JSON format: Raw content (string) - If render is 'json' or absent: Structured data (list/generator) """ # Check if non-JSON format requested render_format = kwargs.get("render") if render_format == "svg": # Pass expect_json=False for raw SVG response req = Request(**self.request_kwargs, expect_json=False).get( add_params=kwargs ) # Return raw content for non-JSON formats return next(req) if render_format != "json" and render_format is not None: raise ValueError(f"Unsupported render format: {render_format}") # Return structured JSON response via parent class return super().list(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/extension.py0000644000175100017510000000505415215017101020227 0ustar00runnerrunner""" Extension framework for pynetbox. Lets third-party NetBox plugins ship custom ``Record`` subclasses and content-type mappings without modifying pynetbox itself. An extension is any object (typically a class) exposing: * ``plugin_name`` (str): the plugin's API URL slug as used under ``/api/plugins/`` — *not* the plugin's Python package name or ``PluginConfig.name``. For example, the ``netbox-acls`` plugin serves ``/api/plugins/access-lists/`` so its ``plugin_name`` is ``"access_lists"`` (or equivalently ``"access-lists"``). Dashes are normalized to underscores on registration, so the same plugin cannot be registered twice under both spellings. The attribute access ``nb.plugins.`` then converts underscores back to dashes for the URL, matching the existing ``PluginsApp`` convention. * ``models``: a namespace (module, class, or any object) from which ``Record`` subclasses can be resolved by title-cased endpoint name — the same lookup used for built-in apps. For ``nb.plugins..branches`` pynetbox will look up ``models.Branches``. If your namespace defines ``__getattr__`` to provide a fallback for endpoints not known at import time, ``models`` must be an *instance*, not a bare class — ``getattr`` on a class only consults the metaclass's ``__getattr__``, never the class's own. See ``pynetbox/extensions/custom_objects.py`` for an example. * ``content_types`` (dict, optional): maps NetBox content-type strings (e.g. ``"netbox_branching.branch"``) to ``Record`` subclasses, so the classes are used when resolving polymorphic nested objects. ## Example ```python from pynetbox import api, Extension, JsonField from pynetbox.core.response import Record from pynetbox.core.endpoint import DetailEndpoint class Branches(Record): @property def merge(self): return DetailEndpoint(self, "merge") class BranchingModels: Branches = Branches class BranchingExtension(Extension): plugin_name = "branching" models = BranchingModels nb = api("http://localhost:8000", token="...", extensions=[BranchingExtension]) branch = nb.plugins.branching.branches.get(1) branch.merge.create(commit=True) ``` """ class Extension: """Base class for pynetbox extensions. Subclassing is optional — any object with the required attributes is accepted. The base class exists so extension authors have an explicit contract to target and so ``isinstance`` checks are available to downstream code that wants them. """ plugin_name: str = "" models = None content_types: dict = {} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/query.py0000644000175100017510000005456315215017101017371 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import concurrent.futures as cf import io import os import json import warnings from packaging import version # NetBox v2 token prefix (introduced in NetBox 4.5.0) TOKEN_PREFIX = "nbt_" def _is_v2_token(token): """Detect if a token is NetBox v2 format. V2 tokens (introduced in NetBox 4.5.0) have the format: nbt_. The nbt_ prefix is used for secrets detection. V1 tokens are simple strings without dots. Returns True if token is v2 format, False otherwise. """ if not token or not token.startswith(TOKEN_PREFIX): return False # Remove nbt_ prefix token_body = token[len(TOKEN_PREFIX) :] # V2 tokens contain a dot separating the ID from the secret return "." in token_body def _is_file_like(obj): if isinstance(obj, (str, bytes)): return False # Check if it's a standard library IO object OR has a callable read method return isinstance(obj, io.IOBase) or ( hasattr(obj, "read") and callable(getattr(obj, "read")) ) def _extract_files(data): """Extract file-like objects from data dict. Returns a tuple of (clean_data, files) where clean_data has file objects removed and files is a dict suitable for requests' files parameter. """ if not isinstance(data, dict): return data, None files = {} clean_data = {} for key, value in data.items(): if _is_file_like(value): # Format: (filename, file_obj, content_type) # Try to get filename from file object, fallback to key filename = getattr(value, "name", None) if filename: # Extract just the filename, not the full path filename = os.path.basename(filename) else: filename = key files[key] = (filename, value) elif isinstance(value, tuple) and len(value) >= 2 and _is_file_like(value[1]): # Already in (filename, file_obj) or (filename, file_obj, content_type) format files[key] = value else: clean_data[key] = value return clean_data, files if files else None def calc_pages(limit, count): """Calculate number of pages required for full results set.""" return int(count / limit) + (limit % count > 0) class RequestError(Exception): """Basic Request Exception. More detailed exception that returns the original requests object for inspection. Along with some attributes with specific details from the requests object. If return is json we decode and add it to the message. ## Examples ```python try: nb.dcim.devices.create(name="destined-for-failure") except pynetbox.RequestError as e: print(e.error) ``` """ def __init__(self, req): if req.status_code == 404: self.message = "The requested url: {} could not be found.".format(req.url) else: try: self.message = "The request failed with code {} {}: {}".format( req.status_code, req.reason, req.json() ) except ValueError: self.message = ( "The request failed with code {} {} but more specific " "details were not returned in json. Check the NetBox Logs " "or investigate this exception's error attribute.".format( req.status_code, req.reason ) ) super().__init__(self.message) self.req = req self.request_body = req.request.body self.base = req.url self.error = req.text def __str__(self): return self.message class AllocationError(Exception): """Allocation Exception. Used with available-ips/available-prefixes when there is no room for allocation and NetBox returns 409 Conflict. """ def __init__(self, req): super().__init__(req) self.req = req self.request_body = req.request.body self.base = req.url self.error = "The requested allocation could not be fulfilled." def __str__(self): return self.error class ContentError(Exception): """Content Exception. If the API URL does not point to a valid NetBox API, the server may return a valid response code, but the content is not json. This exception is raised in those cases. """ def __init__(self, req): super().__init__(req) self.req = req self.request_body = req.request.body self.base = req.url self.error = ( "The server returned invalid (non-json) data. Maybe not a NetBox server?" ) def __str__(self): return self.error class ParameterValidationError(Exception): """API parameter validation Exception. Raised when filter parameters do not match Netbox OpenAPI specification. ## Examples ```python try: nb.dcim.devices.filter(field_which_does_not_exist="destined-for-failure") except pynetbox.ParameterValidationError as e: print(e.error) ``` """ def __init__(self, errors): super().__init__(errors) self.error = f"The request parameter validation returned an error: {errors}" def __str__(self): return self.error class Request: """Creates requests to the Netbox API. Responsible for building the url and making the HTTP(S) requests to Netbox's API. ## Parameters * **base** (str): Base URL passed in api() instantiation. * **filters** (dict, optional): Contains key/value pairs that correlate to the filters a given endpoint accepts. In (e.g. /api/dcim/devices/?name='test') 'name': 'test' would be in the filters dict. """ def __init__( self, base, http_session, filters=None, limit=None, offset=None, key=None, token=None, threading=False, thread_pool_executor=None, max_workers=4, expect_json=True, pagination="offset", ): """Instantiates a new Request object. ## Parameters * **base** (string): Base URL passed in api() instantiation. * **filters** (dict, optional): Contains key/value pairs that correlate to the filters a given endpoint accepts. In (e.g. /api/dcim/devices/?name='test') 'name': 'test' would be in the filters dict. * **key** (int, optional): Database id of the item being queried. * **expect_json** (bool, optional): If True, expects JSON response and sets appropriate Accept header. If False, expects raw content (e.g., SVG, XML) and returns text. Defaults to True. * **pagination** (str or callable, optional): Pagination strategy for list views, either ``"offset"`` (default) or ``"cursor"``. May also be a zero-argument callable returning one of those strings, which is resolved lazily on the first list request so any work it performs (e.g. a server version probe) is deferred until needed. Cursor pagination (NetBox 4.6+) pages with the ``start`` parameter and follows ``next`` links sequentially; it omits the total count and is mutually exclusive with threading. ## Note The `count` attribute is not initialized here. It is set dynamically by the `get()` method when paginating results, or by `get_count()` when explicitly requesting the count. This allows `get_count()` to use `hasattr()` to determine if a count has already been fetched. """ self.base = self.normalize_url(base) self.filters = filters or None self.key = key self.token = token self.http_session = http_session self.url = self.base if not key else "{}{}/".format(self.base, key) self.threading = threading self.thread_pool_executor = ( thread_pool_executor if thread_pool_executor is not None else cf.ThreadPoolExecutor ) self.max_workers = max_workers self.limit = limit self.offset = offset self.expect_json = expect_json self.pagination = pagination def get_openapi(self): """Gets the OpenAPI Spec.""" headers = { "Accept": "application/json", "Content-Type": "application/json", } current_version = version.parse(self.get_version()) if current_version >= version.parse("3.5"): req = self.http_session.get( "{}schema/".format(self.normalize_url(self.base)), headers=headers, ) else: req = self.http_session.get( "{}docs/?format=openapi".format(self.normalize_url(self.base)), headers=headers, ) if req.ok: return req.json() else: raise RequestError(req) def get_version(self): """Gets the API version of NetBox. Issues a GET request to the base URL to read the API version from the response headers. ## Returns Version number as a string. Empty string if version is not present in the headers. ## Raises RequestError if req.ok returns false. """ headers = {"Content-Type": "application/json"} self._add_auth_header(headers) req = self.http_session.get( self.normalize_url(self.base), headers=headers, ) if req.ok or req.status_code == 403: return req.headers.get("API-Version", "") else: raise RequestError(req) def get_status(self): """Gets the status from /api/status/ endpoint in NetBox. ## Returns Dictionary as returned by NetBox. ## Raises RequestError if request is not successful. """ headers = {"Content-Type": "application/json"} self._add_auth_header(headers) req = self.http_session.get( "{}status/".format(self.normalize_url(self.base)), headers=headers, ) if req.ok: return req.json() else: raise RequestError(req) def normalize_url(self, url): """Builds a url for POST actions.""" if url[-1] != "/": return "{}/".format(url) return url def _add_auth_header(self, headers): """Add authorization header to headers dict if token is present. ## Parameters * **headers** (dict): Headers dictionary to update with authorization. """ if self.token: if _is_v2_token(self.token): headers["authorization"] = "Bearer {}".format(self.token) else: headers["authorization"] = "Token {}".format(self.token) def _make_call(self, verb="get", url_override=None, add_params=None, data=None): # Extract any file-like objects from data files = None # Verbs that support request bodies with file uploads body_verbs = ("post", "put", "patch") # Set Accept header based on expected response type if self.expect_json: headers = {"accept": "application/json"} else: headers = {"accept": "*/*"} # Extract files from data for applicable verbs if data is not None and verb in body_verbs: data, files = _extract_files(data) # Set headers based on request type should_be_json_body = not files and ( verb in body_verbs or (verb == "delete" and data) ) if should_be_json_body: headers["Content-Type"] = "application/json" self._add_auth_header(headers) params = {} if not url_override: if self.filters: params.update(self.filters) if add_params: params.update(add_params) if files: # Use multipart/form-data for file uploads req = getattr(self.http_session, verb)( url_override or self.url, headers=headers, params=params, data=data, files=files, ) else: req = getattr(self.http_session, verb)( url_override or self.url, headers=headers, params=params, json=data ) if req.status_code == 409 and verb == "post": raise AllocationError(req) if verb == "delete": if req.ok: return True else: raise RequestError(req) elif req.ok: # Parse response based on expected type if self.expect_json: try: return req.json() except json.JSONDecodeError: raise ContentError(req) else: # Return raw text for non-JSON responses return req.text else: raise RequestError(req) def concurrent_get(self, ret, page_size, page_offsets): futures_to_results = [] with self.thread_pool_executor(max_workers=self.max_workers) as pool: for offset in page_offsets: new_params = {"offset": offset, "limit": page_size} futures_to_results.append( pool.submit(self._make_call, add_params=new_params) ) for future in cf.as_completed(futures_to_results): result = future.result() ret.extend(result["results"]) def _resolve_pagination(self): """Resolve the configured pagination strategy. ``pagination`` may be either a string (``"offset"``/``"cursor"``) or a zero-argument callable returning one. Deferring the call to here keeps any work it does (such as the NetBox version probe behind ``Api._effective_pagination``) out of ``Request`` construction, so a lazily-built ``RecordSet`` that is never iterated makes no extra call. """ pagination = self.pagination return pagination() if callable(pagination) else pagination def get(self, add_params=None): """Makes a GET request. Makes a GET request to NetBox's API, and automatically recurses any paginated results. ## Returns List of `Response` objects returned from the endpoint. ## Raises * RequestError if req.ok returns false. * ContentError if response is not json. """ # Cursor-based pagination (NetBox 4.6+) is used only for full # iteration of a list view. An explicit offset (single requested # page) or a detail route (add_params already set) falls back to # the default offset behavior, since 'start' and 'offset' are # mutually exclusive on the server. use_cursor = ( self._resolve_pagination() == "cursor" and self.offset is None and add_params is None ) if use_cursor and self.filters and "ordering" in self.filters: # Cursor pagination derives the page boundaries from a fixed # server-side ordering, so a caller-supplied 'ordering' filter is # silently ignored by NetBox. Warn rather than let the result come # back in an unexpected order. # stacklevel=3: get() (generator body, resumed on the first # next()) -> RecordSet.__next__/__len__ -> caller. warnings.warn( "ordering has no effect with cursor pagination; results are " "returned in NetBox's fixed cursor order.", stacklevel=3, ) if not add_params and self.limit is not None: add_params = {"limit": self.limit} if use_cursor: # 'start' is the numeric id of the first object to return; # begin at the start and follow the server's next links. add_params["start"] = 0 elif self.limit and self.offset is not None: # if non-zero limit and some offset -> add offset add_params["offset"] = self.offset req = self._make_call(add_params=add_params) if isinstance(req, dict) and req.get("results") is not None: # In cursor mode NetBox omits the count (returns null); it is # fetched lazily by get_count() if len() is requested. self.count = req["count"] if use_cursor: # Sequentially follow next links; each link carries the # next 'start' cursor (last pk + 1) computed by the server. yield from req["results"] while req.get("next"): req = self._make_call(url_override=req["next"]) yield from req["results"] elif self.offset is not None: # only yield requested page results if paginating for i in req["results"]: yield i elif self.threading: ret = req["results"] if req.get("next"): page_size = len(req["results"]) pages = calc_pages(page_size, req["count"]) page_offsets = [ increment * page_size for increment in range(1, pages) ] if pages == 1: req = self._make_call(url_override=req.get("next")) ret.extend(req["results"]) else: self.concurrent_get(ret, page_size, page_offsets) for i in ret: yield i else: first_run = True for i in req["results"]: yield i while req["next"]: # Not worrying about making sure add_params kwargs is # passed in here because results from detail routes aren't # paginated, thus far. if first_run: req = self._make_call( add_params={ "limit": self.limit or req["count"], "offset": len(req["results"]), } ) else: req = self._make_call(url_override=req["next"]) first_run = False for i in req["results"]: yield i elif isinstance(req, list): self.count = len(req) for i in req: yield i else: self.count = len(req) yield req def put(self, data): """Makes PUT request. Makes a PUT request to NetBox's API. ## Parameters * **data** (dict): Contains a dict that will be turned into a json object and sent to the API. ## Returns Dict containing the response from NetBox's API. ## Raises * RequestError if req.ok returns false. * ContentError if response is not json. """ return self._make_call(verb="put", data=data) def post(self, data): """Makes POST request. Makes a POST request to NetBox's API. ## Parameters * **data** (dict): Contains a dict that will be turned into a json object and sent to the API. ## Returns Dict containing the response from NetBox's API. ## Raises * RequestError if req.ok returns false. * AllocationError if req.status_code is 409 (Conflict) as with available-ips and available-prefixes when there is no room for the requested allocation. * ContentError if response is not json. """ return self._make_call(verb="post", data=data) def delete(self, data=None): """Makes DELETE request. Makes a DELETE request to NetBox's API. ## Parameters * **data** (list): Contains a dict that will be turned into a json object and sent to the API. ## Returns True if successful. ## Raises RequestError if req.ok doesn't return True. """ return self._make_call(verb="delete", data=data) def patch(self, data): """Makes PATCH request. Makes a PATCH request to NetBox's API. ## Parameters * **data** (dict): Contains a dict that will be turned into a json object and sent to the API. ## Returns Dict containing the response from NetBox's API. ## Raises * RequestError if req.ok returns false. * ContentError if response is not json. """ return self._make_call(verb="patch", data=data) def options(self): """Makes an OPTIONS request. Makes an OPTIONS request to NetBox's API. ## Returns Dict containing the response from NetBox's API. ## Raises * RequestError if req.ok returns false. * ContentError if response is not json. """ return self._make_call(verb="options") def get_count(self, *args, **kwargs): """Returns object count for query. Makes a query to the endpoint with ``limit=1`` set and only returns the value of the "count" field. ## Returns Int of number of objects query returned. ## Raises * RequestError if req.ok returns false. * ContentError if response is not json. """ # ``count`` may be unset, or set to None when cursor pagination was # used (NetBox omits the count in that mode). In both cases fetch it # explicitly. This uses an offset-style request (no 'start' param), # which always returns the total count. if getattr(self, "count", None) is None: self.count = self._make_call(add_params={"limit": 1, "brief": 1})["count"] return self.count ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/response.py0000644000175100017510000007510615215017101020056 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import copy from urllib.parse import urlsplit import pynetbox.core.app from pynetbox.core.query import Request from pynetbox.core.util import Hashabledict # List of fields that are lists but should be treated as sets. LIST_AS_SET = ("tags", "tagged_vlans") # List fields whose item type is announced via a sibling "_type" # content-type string (NetBox's CableTerminationModelSerializerMixin pattern). # Items are cast to the Record subclass named by that sibling content type. SIBLING_TYPED_LIST_FIELDS = ("link_peers", "connected_endpoints") def get_return(lookup, return_fields=None): """Returns simple representations for items passed to lookup. Used to return a "simple" representation of objects and collections sent to it via lookup. Otherwise, we look to see if lookup is a "choices" field (dict with only 'id' and 'value') or a nested_return. Finally, we check if it's a Record, if so simply return a string. Order is important due to nested_return being self-referential. ## Parameters * **return_fields** (list, optional): A list of fields to reference when calling values on lookup. """ for i in return_fields or ["id", "value", "nested_return"]: if isinstance(lookup, dict) and lookup.get(i): return lookup[i] else: if hasattr(lookup, i): # check if this is a "choices" field record # from a NetBox 2.7 server. if sorted(dict(lookup)) == sorted(["id", "value", "label"]): return getattr(lookup, "value") return getattr(lookup, i) if isinstance(lookup, Record): return str(lookup) else: return lookup def flatten_custom(custom_dict): ret = {} for k, val in custom_dict.items(): current_val = val if isinstance(val, dict): current_val = val.get("id", val) if isinstance(val, list): current_val = [v.get("id", v) if isinstance(v, dict) else v for v in val] ret[k] = current_val return ret class JsonField: """Explicit field type for values that are not to be converted to a Record object.""" _json_field = True class RecordSet: """Iterator containing Record objects. Returned by `Endpoint.all()` and `Endpoint.filter()` methods. Allows iteration of and actions to be taken on the results from the aforementioned methods. Contains Record objects. ## Examples To see how many results are in a query by calling `len()`: ```python x = nb.dcim.devices.all() len(x) # 123 ``` Simple iteration of the results: ```python devices = nb.dcim.devices.all() for device in devices: print(device.name) # test1-leaf1 # test1-leaf2 # test1-leaf3 ``` """ def __init__(self, endpoint, request, **kwargs): self.endpoint = endpoint self.request = request self.response = self.request.get() self._response_cache = [] def __iter__(self): return self def __next__(self): if self._response_cache: return self.endpoint.return_obj( self._response_cache.pop(), self.endpoint.api, self.endpoint ) return self.endpoint.return_obj( next(self.response), self.endpoint.api, self.endpoint ) def __len__(self): try: count = self.request.count except AttributeError: try: self._response_cache.append(next(self.response)) except StopIteration: return 0 count = self.request.count if count is None: # Cursor-based pagination omits the total count; fetch it # explicitly with a separate (offset-based) count request. count = self.request.get_count() return count def update(self, **kwargs): """Updates kwargs onto all Records in the RecordSet and saves these. Updates are only sent to the API if a value were changed, and only for the Records which were changed. ## Returns True if the update succeeded, None if no update were required. ## Examples ```python result = nb.dcim.devices.filter(site_id=1).update(status='active') # True ``` """ updates = [] for record in self: # Update each record and determine if anything was updated for k, v in kwargs.items(): setattr(record, k, v) record_updates = record.updates() if record_updates: # if updated, add the id to the dict and append to list of updates record_updates["id"] = record.id updates.append(record_updates) if updates: return self.endpoint.update(updates) else: return None def delete(self): """Bulk deletes objects in a RecordSet. Allows for batch deletion of multiple objects in a RecordSet. ## Returns True if bulk DELETE operation was successful. ## Examples Deleting offline `devices` on site 1: ```python netbox.dcim.devices.filter(site_id=1, status="offline").delete() ``` """ return self.endpoint.delete(self) class Record: """Create Python objects from NetBox API responses. Creates an object from a NetBox response passed as `values`. Nested dicts that represent other endpoints are also turned into Record objects. All fields are then assigned to the object's attributes. If a missing attr is requested (e.g. requesting a field that's only present on a full response on a Record made from a nested response) then pynetbox will make a request for the full object and return the requested value. ## Examples Default representation of the object is usually its name: ```python x = nb.dcim.devices.get(1) x # test1-switch1 ``` Querying a string field: ```python x = nb.dcim.devices.get(1) x.serial # 'ABC123' ``` Querying a field on a nested object: ```python x = nb.dcim.devices.get(1) x.device_type.model # 'QFX5100-24Q' ``` Casting the object as a dictionary: ```python from pprint import pprint pprint(dict(x)) { 'asset_tag': None, 'cluster': None, 'comments': '', 'config_context': {}, 'created': '2018-04-01', 'custom_fields': {}, 'role': { 'id': 1, 'name': 'Test Switch', 'slug': 'test-switch', 'url': 'http://localhost:8000/api/dcim/device-roles/1/' }, 'device_type': {...}, 'display_name': 'test1-switch1', 'face': {'label': 'Rear', 'value': 1}, 'id': 1, 'name': 'test1-switch1', 'parent_device': None, 'platform': {...}, 'position': 1, 'primary_ip': { 'address': '192.0.2.1/24', 'family': 4, 'id': 1, 'url': 'http://localhost:8000/api/ipam/ip-addresses/1/' }, 'primary_ip4': {...}, 'primary_ip6': None, 'rack': { 'display_name': 'Test Rack', 'id': 1, 'name': 'Test Rack', 'url': 'http://localhost:8000/api/dcim/racks/1/' }, 'site': { 'id': 1, 'name': 'TEST', 'slug': 'TEST', 'url': 'http://localhost:8000/api/dcim/sites/1/' }, 'status': {'label': 'Active', 'value': 1}, 'tags': [], 'tenant': None, 'vc_position': None, 'vc_priority': None, 'virtual_chassis': None } ``` Iterating over a Record object: ```python for i in x: print(i) # ('id', 1) # ('name', 'test1-switch1') # ('display_name', 'test1-switch1') ``` """ url = None # Internal Record metadata that should not be serialized for API updates. # These are object bookkeeping attributes, not NetBox API fields. _INTERNAL_ATTRS = frozenset( [ "api", # API client instance "endpoint", # Endpoint object reference "url", # Object URL (read-only field provided by API) "has_details", # Flag for lazy-loading full details "default_ret", # Default Record class for nested objects ] ) def __init__(self, values, api, endpoint): self.has_details = False self._full_cache = [] self._init_cache = [] self.api = api self.default_ret = Record self.endpoint = ( self._endpoint_from_url(values["url"]) if values and "url" in values and values["url"] else endpoint ) if values: self._parse_values(values) def __getattr__(self, k): """Default behavior for missing attrs. We'll call `full_details()` if we're asked for an attribute we don't have. In order to prevent non-explicit behavior,`k='keys'` is excluded because casting to dict() calls this attr. Dunder attributes are excluded as well: they are never NetBox fields, and probing for them (e.g. pydantic's isinstance check calling `hasattr(obj, '__pydantic_decorators__')`, or copy and pickle machinery) must not trigger a network fetch that would also clobber local modifications. """ if k.startswith("__") and k.endswith("__"): raise AttributeError('object has no attribute "{}"'.format(k)) if self.url: if self.has_details is False and k != "keys": if self.full_details(): ret = getattr(self, k, None) if ret or hasattr(self, k): return ret raise AttributeError('object has no attribute "{}"'.format(k)) def __iter__(self): for i in dict(self._init_cache): cur_attr = getattr(self, i) if isinstance(cur_attr, Record): yield i, dict(cur_attr) elif isinstance(cur_attr, list) and all( isinstance(i, (Record, GenericListObject)) for i in cur_attr ): yield i, [dict(x) for x in cur_attr] else: yield i, cur_attr def __getitem__(self, k): return dict(self)[k] def __str__(self): return ( getattr(self, "name", None) or getattr(self, "label", None) or getattr(self, "display", None) or "" ) def __repr__(self): return str(self) def __getstate__(self): return self.__dict__ def __setstate__(self, d): self.__dict__.update(d) def __key__(self): if hasattr(self, "id"): return (self.endpoint.name, self.id) else: return self.endpoint.name def __hash__(self): return hash(self.__key__()) def __eq__(self, other): if isinstance(other, Record): return self.__key__() == other.__key__() return NotImplemented def _extract_app_endpoint(self, url): """Extract app/endpoint from a NetBox API URL. Extracts the app and endpoint portion from a URL like: https://netbox/api/dcim/rear-ports/12761/ Returns: String like "dcim/rear-ports" """ app_endpoint = "/".join( urlsplit(url).path[len(urlsplit(self.api.base_url).path) :].split("/")[1:3] ) return app_endpoint def _get_obj_class(self, url): """Map API URL to corresponding Record class for cable tracing. Used by TraceableRecord and PathableRecord to deserialize objects encountered in cable trace/path responses. """ # Import here to avoid circular dependency from pynetbox.models.circuits import CircuitTerminations from pynetbox.models.dcim import ( Cables, ConsolePorts, ConsoleServerPorts, FrontPorts, Interfaces, PowerFeeds, PowerOutlets, PowerPorts, RearPorts, ) uri_to_obj_class_map = { "circuits/circuit-terminations": CircuitTerminations, "dcim/cables": Cables, "dcim/console-ports": ConsolePorts, "dcim/console-server-ports": ConsoleServerPorts, "dcim/front-ports": FrontPorts, "dcim/interfaces": Interfaces, "dcim/power-feeds": PowerFeeds, "dcim/power-outlets": PowerOutlets, "dcim/power-ports": PowerPorts, "dcim/rear-ports": RearPorts, } app_endpoint = self._extract_app_endpoint(url) return uri_to_obj_class_map.get(app_endpoint, Record) def _add_cache(self, item): key, value = item self._init_cache.append((key, get_return(value))) def _parse_values(self, values): """Parses values init arg. Parses values dict at init and sets object attributes with the values within. """ def generic_list_parser(key_name, list_item): from pynetbox.models.mapper import CONTENT_TYPE_MAPPER content_type_mapper = getattr( self.api, "_content_type_mapper", CONTENT_TYPE_MAPPER ) if ( isinstance(list_item, dict) and "object_type" in list_item and "object" in list_item ): lookup = list_item["object_type"] if model := content_type_mapper.get(lookup, None): record = model(list_item["object"], self.api, self.endpoint) return GenericListObject(record) return list_item def list_parser(key_name, list_item): if isinstance(list_item, dict): lookup = getattr(self.__class__, key_name, None) if not isinstance(lookup, list): # This is *list_parser*, so if the custom model field is not # a list (or is not defined), just return the default model return self.default_ret(list_item, self.api, self.endpoint) else: model = lookup[0] return model(list_item, self.api, self.endpoint) return list_item def sibling_typed_list_parser(list_item, content_type): from pynetbox.models.mapper import CONTENT_TYPE_MAPPER content_type_mapper = getattr( self.api, "_content_type_mapper", CONTENT_TYPE_MAPPER ) if isinstance(list_item, dict): if model := content_type_mapper.get(content_type, None): return model(list_item, self.api, self.endpoint) return self.default_ret(list_item, self.api, self.endpoint) return list_item for k, v in values.items(): if isinstance(v, dict): lookup = getattr(self.__class__, k, None) if k in ["custom_fields", "local_context_data"] or hasattr( lookup, "_json_field" ): self._add_cache((k, copy.deepcopy(v))) setattr(self, k, v) continue if isinstance(lookup, type) and issubclass(lookup, Record): v = lookup(v, self.api, self.endpoint) else: v = self.default_ret(v, self.api, self.endpoint) self._add_cache((k, v)) elif isinstance(v, list): lookup = getattr(self.__class__, k, None) # An explicit JsonField marker means the column is raw JSON # (e.g. a plugin field holding a list of dicts). Keep it as-is # instead of coercing each dict into a nested Record, which # would break serialize()/save() (no id on plain JSON dicts). if hasattr(lookup, "_json_field"): self._add_cache((k, copy.deepcopy(v))) setattr(self, k, v) continue # check if GFK if len(v) and isinstance(v[0], dict) and "object_type" in v[0]: v = [generic_list_parser(k, i) for i in v] # An unmapped object_type (e.g. a plugin whose extension # hasn't been registered) falls through as the raw dict, # so cache it directly instead of assuming .serialize(). to_cache = [ i.serialize() if hasattr(i, "serialize") else copy.deepcopy(i) for i in v ] elif k == "constraints": # Permissions constraints can be either dict or list to_cache = copy.deepcopy(v) elif ( k in SIBLING_TYPED_LIST_FIELDS and len(v) and isinstance(v[0], dict) and isinstance(values.get(f"{k}_type"), str) ): # NetBox cable-termination pattern: link_peers/ # connected_endpoints carry their item type in the sibling # link_peers_type/connected_endpoints_type field rather # than per-item. Cast each item to the model named by # that sibling content type. v = [sibling_typed_list_parser(i, values[f"{k}_type"]) for i in v] to_cache = list(v) else: v = [list_parser(k, i) for i in v] to_cache = list(v) self._add_cache((k, to_cache)) else: self._add_cache((k, v)) setattr(self, k, v) def _endpoint_from_url(self, url): url_path = urlsplit(url).path base_url_path_parts = urlsplit(self.api.base_url).path.split("/") if len(base_url_path_parts) > 2: # There are some extra directories in the path, remove them from url extra_path = "/".join(base_url_path_parts[:-1]) url_path = url_path[len(extra_path) :] split_url_path = url_path.split("/") if split_url_path[2] == "plugins": app = "plugins/{}".format(split_url_path[3]) name = split_url_path[4] else: app, name = split_url_path[2:4] return getattr(pynetbox.core.app.App(self.api, app), name) def full_details(self): """Queries the hyperlinked endpoint if 'url' is defined. This method will populate the attributes from the detail endpoint when it's called. Sets the class-level `has_details` attribute when it's called to prevent being called more than once. :returns: True """ if self.url: req = Request( base=self.url, token=self.api.token, http_session=self.api.http_session, ) self._parse_values(next(req.get())) self.has_details = True return True return False def serialize(self, nested=False, init=False): """Serializes an object Pulls all the attributes in an object and creates a dict that can be turned into the json that netbox is expecting. If an attribute's value is a ``Record`` type it's replaced with the ``id`` field of that object. When ``init=False`` (default), includes both original fields from the API response and any fields that have been set on the object after initialization. This allows proper change detection for fields set to None or other values. When ``init=True``, returns only the original fields from the initial API response, used for comparing against the current state to detect changes. .. note:: Using this to get a dictionary representation of the record is discouraged. It's probably better to cast to dict() instead. See Record docstring for example. :returns: dict. """ if nested: return get_return(self) # Determine which fields to serialize if init: # For initial state, use only _init_cache init_cache_dict = dict(self._init_cache) fields_to_serialize = init_cache_dict.keys() init_vals = init_cache_dict else: # For current state, include all fields (original + modified). # Preserve init_cache insertion order, then append any new # attributes set after init, so the serialized dict has a # deterministic key order across init=True/False calls (the # str-based list comparison in _diff() depends on it). init_cache_keys = [k for k, _ in self._init_cache] init_cache_key_set = set(init_cache_keys) extra_keys = [ k for k in self.__dict__.keys() if not k.startswith("_") and k not in self._INTERNAL_ATTRS and k not in init_cache_key_set ] fields_to_serialize = init_cache_keys + extra_keys init_vals = {} # Not used when init=False ret = {} for i in fields_to_serialize: current_val = getattr(self, i, None) if not init else init_vals.get(i) if i == "custom_fields": ret[i] = flatten_custom(current_val) else: if isinstance(current_val, Record): current_val = getattr(current_val, "serialize")(nested=True) if isinstance(current_val, list): serialized_list = [] for v in current_val: if isinstance(v, GenericListObject): v = v.serialize() elif isinstance(v, Record): # FK-style list items (e.g. tagged_vlans) collapse to # their id. NetBox 4.5+ also returns nested mapping # objects without an id of their own (e.g. # FrontPort.rear_ports items); preserve those as dicts # so the API round-trips correctly. v = v.id if hasattr(v, "id") else v.serialize(init=init) serialized_list.append(v) current_val = serialized_list if i in LIST_AS_SET and ( all([isinstance(v, str) for v in current_val]) or all([isinstance(v, int) for v in current_val]) ): current_val = list(dict.fromkeys(current_val)) ret[i] = current_val return ret def _diff(self): def fmt_dict(k, v): if isinstance(v, dict): return k, Hashabledict(v) if isinstance(v, list): return k, ",".join(map(str, v)) return k, v current_serialized = self.serialize() init_serialized = self.serialize(init=True) # custom_fields use merge semantics on PATCH: NetBox leaves any custom # field omitted from an update unchanged. Restrict the comparison to the # keys present in the current value so that assigning a subset of custom # fields isn't seen as removing the others (issue #748). To clear a # custom field the caller still sets it explicitly to None. Assigning an # empty dict is therefore a no-op (it touches no keys), matching NetBox's # merge semantics where an empty custom_fields body changes nothing. current_cf = current_serialized.get("custom_fields") init_cf = init_serialized.get("custom_fields") if isinstance(current_cf, dict) and isinstance(init_cf, dict): init_serialized = { **init_serialized, "custom_fields": { k: v for k, v in init_cf.items() if k in current_cf }, } current = Hashabledict({fmt_dict(k, v) for k, v in current_serialized.items()}) init = Hashabledict( {fmt_dict(k, v) for k, v in init_serialized.items()} ) return set([i[0] for i in set(current.items()) ^ set(init.items())]) def updates(self): """Compiles changes for an existing object into a dict. Takes a diff between the objects current state and its state at init and returns them as a dictionary, which will be empty if no changes. :returns: dict. :example: >>> x = nb.dcim.devices.get(name='test1-a3-tor1b') >>> x.serial '' >>> x.serial = '1234' >>> x.updates() {'serial': '1234'} >>> """ if self.id: diff = self._diff() if diff: serialized = self.serialize() return {i: serialized[i] for i in diff} return {} def save(self): """Saves changes to an existing object. Takes a diff between the objects current state and its state at init and sends them as a dictionary to Request.patch(). :returns: True if PATCH request was successful. :example: >>> x = nb.dcim.devices.get(name='test1-a3-tor1b') >>> x.serial '' >>> x.serial = '1234' >>> x.save() True >>> """ updates = self.updates() if updates: req = Request( key=self.id, base=self.endpoint.url, token=self.api.token, http_session=self.api.http_session, ) result = req.patch(updates) if result: # Update object state with response from PATCH to keep cache in sync self._parse_values(result) return True return False def update(self, data): """Update an object with a dictionary. Accepts a dict and uses it to update the record and call save(). For nested and choice fields you'd pass an int the same as if you were modifying the attribute and calling save(). :arg dict data: Dictionary containing the k/v to update the record object with. :returns: True if PATCH request was successful. :example: >>> x = nb.dcim.devices.get(1) >>> x.update({ ... "name": "test-switch2", ... "serial": "ABC321", ... }) True """ for k, v in data.items(): setattr(self, k, v) return self.save() def delete(self): """Deletes an existing object. :returns: True if DELETE operation was successful. :example: >>> x = nb.dcim.devices.get(name='test1-a3-tor1b') >>> x.delete() True >>> """ req = Request( key=self.id, base=self.endpoint.url, token=self.api.token, http_session=self.api.http_session, ) return True if req.delete() else False class PathableRecord(Record): """Record class for objects that support cable path tracing via /paths endpoint. Front ports, rear ports, and circuit terminations use the /paths endpoint to show complete cable paths from origin to destination. """ def _build_endpoint_object(self, endpoint_data): if not endpoint_data: return None return_obj_class = self._get_obj_class(endpoint_data["url"]) return return_obj_class(endpoint_data, self.endpoint.api, self.endpoint) def paths(self): """Return all cable paths traversing this pass-through port. Returns a list of dictionaries, each containing: - origin: The starting endpoint of the path (or None if not connected) - destination: The ending endpoint of the path (or None if not connected) - path: List of path segments, where each segment is a list of Record objects (similar to the trace() endpoint structure) """ req = Request( key=str(self.id) + "/paths", base=self.endpoint.url, token=self.api.token, http_session=self.api.http_session, ).get() ret = [] for path_data in req: path_segments = [] for segment_data in path_data.get("path", []): segment_objects = [] if isinstance(segment_data, list): for item_data in segment_data: segment_obj = self._build_endpoint_object(item_data) if segment_obj: segment_objects.append(segment_obj) else: segment_obj = self._build_endpoint_object(segment_data) if segment_obj: segment_objects.append(segment_obj) path_segments.append(segment_objects) origin = self._build_endpoint_object(path_data.get("origin")) destination = self._build_endpoint_object(path_data.get("destination")) ret.append({ "origin": origin, "destination": destination, "path": path_segments, }) return ret class GenericListObject: def __init__(self, record): from pynetbox.models.mapper import TYPE_CONTENT_MAPPER type_content_mapper = getattr( record.api, "_type_content_mapper", TYPE_CONTENT_MAPPER ) self.object = record self.object_id = record.id self.object_type = type_content_mapper.get(record.__class__) def __repr__(self): return str(self.object) def serialize(self): ret = {k: getattr(self, k) for k in ["object_id", "object_type"]} return ret def __getattr__(self, k): return getattr(self.object, k) def __iter__(self): for i in ["object_id", "object_type", "object"]: cur_attr = getattr(self, i) if isinstance(cur_attr, Record): cur_attr = dict(cur_attr) yield i, cur_attr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/core/util.py0000644000175100017510000000012715215017101017164 0ustar00runnerrunnerclass Hashabledict(dict): def __hash__(self): return hash(frozenset(self)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9486997 pynetbox-7.8.0/pynetbox/extensions/0000755000175100017510000000000015215017112017106 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/extensions/__init__.py0000644000175100017510000000072515215017101021221 0ustar00runnerrunner""" Shipped pynetbox extensions for popular NetBox plugins. Each module here exposes an `Extension` subclass that can be passed to `pynetbox.api(extensions=[...])`. See `pynetbox.core.extension` for the extension contract and `docs/extensions.md` for a user-facing guide. """ from pynetbox.extensions.branching import BranchingExtension from pynetbox.extensions.custom_objects import CustomObjectsExtension __all__ = ("BranchingExtension", "CustomObjectsExtension") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/extensions/branching.py0000644000175100017510000002034715215017101021417 0ustar00runnerrunner""" pynetbox extension for the [NetBox branching plugin][branching]. [branching]: https://github.com/netboxlabs/netbox-branching Wires up the plugin's per-branch detail actions (`merge`, `sync`, `revert`, `archive`) as Pythonic methods on the `Branches` record, and marks the JSON fields on `Changes` (ChangeDiff) so they round-trip as Python dicts/lists instead of being mangled into nested `Record` objects. ## Usage ```python import pynetbox from pynetbox.extensions import BranchingExtension nb = pynetbox.api( "http://localhost:8000", token="...", extensions=[BranchingExtension], ) branch = nb.plugins.branching.branches.create(name="my-branch") # (poll branch.status until 'ready' — see docs/branching.md) # Synchronize the branch from main, merge it, or revert a merged branch. # Each returns a `Jobs` record representing the background job NetBox # enqueues to do the actual work. sync_job = branch.sync(commit=True) merge_job = branch.merge(commit=True) revert_job = branch.revert(commit=True) # Archive a merged branch (no background job — returns the updated branch). branch.archive() ``` ## Conflict handling `sync` and `merge` return HTTP 409 when conflicting `ChangeDiff` objects exist and have not been acknowledged. This surfaces in pynetbox as a `RequestError`; inspect `error.error` for the conflict payload, or pass `acknowledge_conflicts=True` after reviewing the conflicts to proceed. ```python import pynetbox try: branch.merge(commit=True) except pynetbox.RequestError as exc: if exc.req.status_code == 409: # exc.error is the conflicts payload; inspect, then either resolve # or acknowledge and retry. branch.merge(commit=True, acknowledge_conflicts=True) else: raise ``` ## Listing branchable models The plugin also exposes a `branchable-models` list endpoint enumerating which NetBox models support branching: ```python for model in nb.plugins.branching.branchable_models.all(): print(model.app_label, model.model) ``` """ from pynetbox.core.extension import Extension from pynetbox.core.query import Request from pynetbox.core.response import JsonField, Record from pynetbox.models.core import Jobs def _post_detail(parent, action_name, data=None, return_cls=None, api=None): """POST to //// and wrap the response. Used by Branches.{merge,sync,revert,archive}. We don't go through DetailEndpoint here because we want to (a) return a different Record class than the parent endpoint's, and (b) keep the call sites as simple method invocations rather than `.create()` chains. """ url = "{}/{}/{}/".format(parent.endpoint.url, parent.id, action_name) resp = Request( base=url, token=parent.api.token, http_session=parent.api.http_session, ).post(data or {}) if return_cls is None: return resp return return_cls(resp, api or parent.api, parent.endpoint) class Branches(Record): """A NetBox branch. Adds the plugin-specific detail actions as direct methods. Each returns a `Jobs` record for the background job NetBox enqueues, except `archive`, which returns the updated `Branches` record. """ def sync(self, commit=True, acknowledge_conflicts=False): """Enqueue a background job to sync this branch from main. ## Parameters * **commit** (bool): If False, perform a dry-run without persisting changes. Defaults to True. * **acknowledge_conflicts** (bool): Proceed even when unresolved `ChangeDiff` conflicts exist. Defaults to False. ## Returns A `Jobs` record for the enqueued sync job. ## Raises `RequestError`: 409 if conflicts exist and have not been acknowledged; 403 if the user lacks `sync_branch` permission. """ return _post_detail( self, "sync", data={"commit": commit, "acknowledge_conflicts": acknowledge_conflicts}, return_cls=Jobs, ) def merge(self, commit=True, acknowledge_conflicts=False): """Enqueue a background job to merge this branch into main. ## Parameters * **commit** (bool): If False, perform a dry-run without persisting changes. Defaults to True. * **acknowledge_conflicts** (bool): Proceed even when unresolved `ChangeDiff` conflicts exist. Defaults to False. ## Returns A `Jobs` record for the enqueued merge job. ## Raises `RequestError`: 409 if conflicts exist and have not been acknowledged; 403 if the user lacks `merge_branch` permission; 400 if the branch is not in a mergeable state. """ return _post_detail( self, "merge", data={"commit": commit, "acknowledge_conflicts": acknowledge_conflicts}, return_cls=Jobs, ) def revert(self, commit=True): """Enqueue a background job to revert this merged branch. ## Parameters * **commit** (bool): If False, perform a dry-run without persisting changes. Defaults to True. ## Returns A `Jobs` record for the enqueued revert job. ## Raises `RequestError`: 400 if the branch has not been merged; 403 if the user lacks `revert_branch` permission. """ return _post_detail( self, "revert", data={"commit": commit}, return_cls=Jobs ) def archive(self): """Archive this merged branch, deprovisioning its schema. Returns the updated `Branches` record. Unlike sync/merge/revert, archive is synchronous — no background job is enqueued. ## Raises `RequestError`: 400 if the branch has not been merged or cannot be archived; 403 if the user lacks `archive_branch` permission. """ updated = _post_detail(self, "archive") # Refresh the local record with the server's view of the branch. self._parse_values(updated) return self @property def changes(self): """Return this branch's `ChangeDiff`s as a `RecordSet`. Convenience for ``nb.plugins.branching.changes.filter( branch_id=branch.id)``. `ChangeDiff` is a top-level endpoint on the branching plugin, so this delegates there with the branch filter applied. Each item is a `Changes` record. """ return self.api.plugins.branching.changes.filter(branch_id=self.id) class BranchEvents(Record): """A read-only branch event (creation, sync, merge, etc.).""" pass class Changes(Record): """A single object change recorded against a branch. The plugin's `ChangeDiff` model has several JSON-typed fields that the default pynetbox deserialization would otherwise mangle into nested `Record` objects. Marking them with `JsonField` keeps them as plain dicts/lists. """ conflicts = JsonField diff = JsonField original_data = JsonField modified_data = JsonField current_data = JsonField class BranchableModels(Record): """A NetBox model that supports branching. The `branchable-models` endpoint returns lightweight `{app_label, model, verbose_name, verbose_name_plural}` entries without an `id` or `url`, so this record acts purely as a typed wrapper. """ pass class _Models: """Namespace for endpoint name → Record class lookups. pynetbox's endpoint resolver does ``getattr(models, "Branches", Record)`` for ``nb.plugins.branching.branches``, so each attribute below is named after the title-cased endpoint name (dashes become CamelCase). """ Branches = Branches BranchEvents = BranchEvents Changes = Changes BranchableModels = BranchableModels class BranchingExtension(Extension): """pynetbox extension for the netbox-branching plugin. Register with: ```python import pynetbox from pynetbox.extensions import BranchingExtension nb = pynetbox.api(url, token="...", extensions=[BranchingExtension]) ``` See the module docstring for usage examples. """ plugin_name = "branching" models = _Models content_types = { "netbox_branching.branch": Branches, "netbox_branching.branchevent": BranchEvents, "netbox_branching.changediff": Changes, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/extensions/custom_objects.py0000644000175100017510000001714415215017101022510 0ustar00runnerrunner""" pynetbox extension for the [NetBox Custom Objects plugin][custom-objects]. [custom-objects]: https://github.com/netboxlabs/netbox-custom-objects Wires up the plugin's records with the right JSON-field markers (so columns like ``related_object_filter`` and ``schema_document`` round-trip as plain dicts/lists), provides helpers for the plugin's non-CRUD endpoints (``schema/preview/``, ``schema/apply/``, ``linked-objects/``), and falls back to a ``CustomObject`` record for dynamic per-type endpoints (``/api/plugins/custom-objects//``). ## Usage ```python import pynetbox from pynetbox.extensions import CustomObjectsExtension nb = pynetbox.api( "http://localhost:8000", token="...", extensions=[CustomObjectsExtension], ) # Custom Object Types: schema_document and fields are JSON-typed. cot = nb.plugins.custom_objects.custom_object_types.get(1) cot.schema_document # dict, not a Record cot.fields[0].related_object_filter # dict, not '' (see issue #751) # Per-type custom objects: any attribute not statically known on the # extension resolves to a CustomObject record class. for obj in nb.plugins.custom_objects.cidr_list.all(): print(obj.name, obj.serialize()) # linked-objects: standard filter() semantics. hits = nb.plugins.custom_objects.linked_objects.filter( object_type="dcim.device", object_id=5, ) # schema/preview and schema/apply: module-level helpers. from pynetbox.extensions.custom_objects import schema_preview, schema_apply diff = schema_preview(nb, {"schema_version": "1", "types": [...]}) result = schema_apply(nb, {"schema_version": "1", "types": [...]}, allow_destructive=False) ``` """ from pynetbox.core.extension import Extension from pynetbox.core.query import Request from pynetbox.core.response import JsonField, Record def _plugin_request(api, path): """Build a `Request` bound to a plugin sub-path under custom-objects. `path` is appended verbatim to ``/plugins/custom-objects/`` and must include the trailing slash NetBox expects. """ slug = CustomObjectsExtension.plugin_name.replace("_", "-") base = "{}/plugins/{}/{}".format(api.base_url, slug, path) return Request(base=base, token=api.token, http_session=api.http_session) class CustomObjectTypeFields(Record): """A field definition on a Custom Object Type. Both `related_object_filter` (issue #751) and `related_object_type` (``{id, app_label, model}`` from the plugin's SerializerMethodField) are dict-valued and marked so they round-trip as plain dicts. The plural `related_object_types` is a list whose server-side items are ``{id, app_label, model}`` dicts. `JsonField` only applies to dict values, so list items deserialize into base `Record`s — ``.id`` / ``.app_label`` / ``.model`` remain accessible. """ related_object_filter = JsonField related_object_type = JsonField class CustomObjectTypes(Record): """A Custom Object Type definition. `schema_document` is a JSON blob (the document the plugin uses to declaratively reconstruct a type's schema) and is marked so it survives the round trip. `fields` is a list of nested `CustomObjectTypeFields` records, which the existing list-parser handles when given a class as the list element. """ schema_document = JsonField fields = [CustomObjectTypeFields] class CustomObject(Record): """A single custom object of an arbitrary user-defined type. The serializer is generated per type and the response shape depends on the type's field definitions, so this class intentionally adds no new field-level handling beyond marking the optional `_context` metadata column (which the server emits as a nested dict when the type has designated context fields). The nested `custom_object_type` attribute resolves to a `CustomObjectTypes` record so attribute access (``obj.custom_object_type.slug``) works as expected. """ custom_object_type = CustomObjectTypes _context = JsonField class LinkedObjects(Record): """An entry from the ``linked-objects/`` endpoint. Each row reports a custom object that references some other NetBox object via an Object/MultiObject field, in the shape:: { "custom_object_type": {...}, "field_name": "...", "object": {...} } Both nested dicts are marked as JSON because their schemas vary by type and pynetbox would otherwise wrap them in generic Records that swallow the unfamiliar keys. """ custom_object_type = JsonField object = JsonField class _Models: """Models namespace for the custom-objects extension. Explicit attributes cover the three statically known endpoints. The `__getattr__` fallback returns `CustomObject` for any other name so that dynamic per-type endpoints (e.g. ``nb.plugins.custom_objects.cidr_list``) deserialize into typed records without each Custom Object Type having to be pre-registered. A bare class with `__getattr__` would not work — `getattr(cls, name)` only consults the metaclass's `__getattr__`, not the class's own. Using an instance puts the fallback on the right code path. """ CustomObjectTypes = CustomObjectTypes CustomObjectTypeFields = CustomObjectTypeFields LinkedObjects = LinkedObjects def __getattr__(self, name): return CustomObject def schema_preview(api, schema_document): """Preview the diff a `schema_document` would produce. POSTs the document to ``/plugins/custom-objects/schema/preview/`` and returns the server's diff response (a dict with a ``diffs`` key). No DB changes are made. ## Parameters * **api** (Api): The pynetbox client instance. * **schema_document** (dict): A COT schema document conforming to the plugin's ``cot_schema_v1.json``. ## Returns dict: The diff response. ## Raises `RequestError`: 400 if the document fails JSON Schema validation. """ return _plugin_request(api, "schema/preview/").post(schema_document) def schema_apply(api, schema_document, allow_destructive=False): """Apply a `schema_document` against the live DB. POSTs ``{"schema": schema_document, "allow_destructive": ...}`` to ``/plugins/custom-objects/schema/apply/`` and returns the server's response (``{"applied": True, "diffs": [...]}``). ## Parameters * **api** (Api): The pynetbox client instance. * **schema_document** (dict): A COT schema document. * **allow_destructive** (bool): Permit ``REMOVE`` field operations that drop DB columns. Defaults to ``False``. ## Returns dict: The apply response. ## Raises `RequestError`: 409 when the document contains destructive changes and ``allow_destructive`` is False; 400 for circular dependencies, unresolvable references, or invalid schema; 403 if the user lacks the required CustomObjectType permissions. """ payload = {"schema": schema_document, "allow_destructive": allow_destructive} return _plugin_request(api, "schema/apply/").post(payload) class CustomObjectsExtension(Extension): """pynetbox extension for the netbox-custom-objects plugin. Register with: ```python import pynetbox from pynetbox.extensions import CustomObjectsExtension nb = pynetbox.api(url, token="...", extensions=[CustomObjectsExtension]) ``` See the module docstring for usage examples. """ plugin_name = "custom_objects" models = _Models() content_types = { "netbox_custom_objects.customobjecttype": CustomObjectTypes, "netbox_custom_objects.customobjecttypefield": CustomObjectTypeFields, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9496882 pynetbox-7.8.0/pynetbox/models/0000755000175100017510000000000015215017112016172 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/__init__.py0000644000175100017510000000000015215017101020267 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/circuits.py0000644000175100017510000000345315215017101020374 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.response import PathableRecord, Record class Circuits(Record): def __str__(self): return self.cid class CircuitTerminations(PathableRecord): """Circuit Termination Record with cable path tracing support. Circuit terminations support cable path tracing via the paths() method, which returns all cable paths traversing this circuit termination point. """ def __str__(self): return self.circuit.cid class VirtualCircuits(Record): """Virtual Circuit Record. Virtual circuits represent L2VPN-like connections delivered across one or more physical circuits. """ def __str__(self): return self.cid class VirtualCircuitTerminations(PathableRecord): """Virtual Circuit Termination Record with cable path tracing support. Virtual circuit terminations support cable path tracing via the paths() method, which returns all cable paths traversing this termination point. """ def __str__(self): return self.virtual_circuit.cid class ProviderNetworks(Record): """Provider Network Record. Represents an upstream provider's network — a connectivity endpoint delivered by a provider that can terminate a circuit without an on-premise device. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/core.py0000644000175100017510000000247215215017101017477 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.endpoint import DetailEndpoint from pynetbox.core.response import JsonField, Record class DataSources(Record): @property def sync(self): """Represents the ``sync`` detail endpoint. Returns a DetailEndpoint object that is the interface for triggering a sync of the data source. ## Returns DetailEndpoint object. ## Examples ```python data_source = nb.core.data_sources.get(123) data_source.sync.create() ``` """ return DetailEndpoint(self, "sync") class Jobs(Record): pass class ObjectChanges(Record): object_data = JsonField postchange_data = JsonField prechange_data = JsonField def __str__(self): return self.request_id ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/dcim.py0000644000175100017510000001471115215017101017462 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.endpoint import ( DetailEndpoint, RODetailEndpoint, ROMultiFormatDetailEndpoint, ) from pynetbox.core.query import Request from pynetbox.core.response import JsonField, PathableRecord, Record from pynetbox.models.circuits import Circuits from pynetbox.models.ipam import IpAddresses class TraceableRecord(Record): def _build_termination_data(self, termination_list): terminations_data = [] for hop_item_data in termination_list: return_obj_class = self._get_obj_class(hop_item_data["url"]) terminations_data.append( return_obj_class(hop_item_data, self.endpoint.api, self.endpoint) ) return terminations_data def trace(self): req = Request( key=str(self.id) + "/trace", base=self.endpoint.url, token=self.api.token, http_session=self.api.http_session, ).get() ret = [] for a_terminations_data, cable_data, b_terminations_data in req: ret.append(self._build_termination_data(a_terminations_data)) if not cable_data: ret.append(cable_data) else: return_obj_class = self._get_obj_class(cable_data["url"]) ret.append( return_obj_class(cable_data, self.endpoint.api, self.endpoint) ) ret.append(self._build_termination_data(b_terminations_data)) return ret class DeviceTypes(Record): def __str__(self): return self.model class Devices(Record): """Devices Object. Represents a device response from netbox. ## Attributes * **primary_ip, ip4, ip6** (list): Tells __init__ in Record() to take the `primary_ip` field's value from the API response and return an initialized list of IpAddress objects * **device_type** (obj): Tells __init__ in Record() to take the `device_type` field's value from the API response and return an initialized DeviceType object """ has_details = True device_type = DeviceTypes primary_ip = IpAddresses primary_ip4 = IpAddresses primary_ip6 = IpAddresses oob_ip = IpAddresses local_context_data = JsonField config_context = JsonField @property def napalm(self): """Represents the ``napalm`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing response from the napalm endpoint. ## Returns DetailEndpoint object. ## Examples ```python device = nb.ipam.devices.get(123) device.napalm.list(method='get_facts') # {"get_facts": {"interface_list": ["ge-0/0/0"]}} ``` """ return RODetailEndpoint(self, "napalm") @property def render_config(self): """Represents the ``render-config`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing response from the render-config endpoint. ## Returns DetailEndpoint object. ## Examples ```python device = nb.ipam.devices.get(123) device.render_config.create() ``` """ return DetailEndpoint(self, "render-config") class InterfaceConnections(Record): def __str__(self): return self.interface_a.name class InterfaceConnection(Record): def __str__(self): return self.interface.name class Interfaces(TraceableRecord): interface_connection = InterfaceConnection class PowerFeeds(TraceableRecord): pass class PowerOutlets(TraceableRecord): device = Devices class PowerPorts(TraceableRecord): device = Devices class ConsolePorts(TraceableRecord): device = Devices class ConsoleServerPorts(TraceableRecord): device = Devices class RackReservations(Record): def __str__(self): return self.description class VirtualChassis(Record): master = Devices class RUs(Record): device = Devices class FrontPorts(PathableRecord): device = Devices class RearPorts(PathableRecord): device = Devices class Racks(Record): @property def units(self): """Represents the ``units`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing response from the units endpoint. ## Returns DetailEndpoint object. ## Examples ```python rack = nb.dcim.racks.get(123) rack.units.list() # {"get_facts": {"interface_list": ["ge-0/0/0"]}} ``` """ return RODetailEndpoint(self, "units", custom_return=RUs) @property def elevation(self): """Represents the ``elevation`` detail endpoint. Returns a multi-format endpoint supporting both JSON and SVG responses. The elevation endpoint provides rack unit information and can render graphical elevation views. ## Returns ROMultiFormatDetailEndpoint object supporting JSON and SVG formats. ## Examples ```python rack = nb.dcim.racks.get(123) # Get rack units as JSON (list of RU objects) rack.elevation.list() # Get elevation as SVG diagram svg = rack.elevation.list(render='svg') ``` """ return ROMultiFormatDetailEndpoint(self, "elevation", custom_return=RUs) class Termination(Record): def __str__(self): # hacky check to see if we're a circuit termination to # avoid another call to NetBox because of a non-existent attr # in self.name if "circuit" in str(self.url): return self.circuit.cid return self.name device = Devices circuit = Circuits class Cables(Record): def __str__(self): if len(self.a_terminations) == 1 and len(self.b_terminations) == 1: return "{} <> {}".format(self.a_terminations[0], self.b_terminations[0]) return "Cable #{}".format(self.id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/extras.py0000644000175100017510000000122415215017101020047 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.response import JsonField, Record class ConfigContexts(Record): data = JsonField ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/ipam.py0000644000175100017510000001302615215017101017472 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.endpoint import DetailEndpoint from pynetbox.core.response import Record class IpAddresses(Record): def __str__(self): return str(self.address) class IpRanges(Record): def __str__(self): return str(self.display) @property def available_ips(self): """Represents the ``available-ips`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing and creating IP addresses inside an ip range. ## Returns DetailEndpoint object. ## Examples ```python ip_range = nb.ipam.ip_ranges.get(24) ip_range.available_ips.list() # [10.0.0.1/24, 10.0.0.2/24, 10.0.0.3/24, 10.0.0.4/24, 10.0.0.5/24, ...] ``` To create a single IP: ```python ip_range = nb.ipam.ip_ranges.get(24) ip_range.available_ips.create() # 10.0.0.1/24 ``` To create multiple IPs: ```python ip_range = nb.ipam.ip_ranges.get(24) create = ip_range.available_ips.create([{} for i in range(2)]) # [10.0.0.2/24, 10.0.0.3/24] ``` """ return DetailEndpoint(self, "available-ips", custom_return=IpAddresses) class Prefixes(Record): def __str__(self): return str(self.prefix) @property def available_ips(self): """Represents the ``available-ips`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing and creating IP addresses inside a prefix. ## Returns DetailEndpoint object. ## Examples ```python prefix = nb.ipam.prefixes.get(24) prefix.available_ips.list() # [10.0.0.1/24, 10.0.0.2/24, 10.0.0.3/24, 10.0.0.4/24, 10.0.0.5/24, ...] ``` To create a single IP: ```python prefix = nb.ipam.prefixes.get(24) prefix.available_ips.create() # 10.0.0.1/24 ``` To create multiple IPs: ```python prefix = nb.ipam.prefixes.get(24) create = prefix.available_ips.create([{} for i in range(2)]) # [10.0.0.2/24, 10.0.0.3/24] ``` """ return DetailEndpoint(self, "available-ips", custom_return=IpAddresses) @property def available_prefixes(self): """Represents the ``available-prefixes`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing and creating prefixes inside a parent prefix. Very similar to `available_ips`, except that dict (or list of dicts) passed to `.create()` needs to have a `prefix_length` key/value specified. ## Returns DetailEndpoint object. ## Examples ```python prefix = nb.ipam.prefixes.get(3) prefix # 10.0.0.0/16 prefix.available_prefixes.list() # [10.0.1.0/24, 10.0.2.0/23, 10.0.4.0/22, 10.0.8.0/21, 10.0.16.0/20, 10.0.32.0/19, 10.0.64.0/18, 10.0.128.0/17] ``` Creating a single child prefix: ```python prefix = nb.ipam.prefixes.get(1) prefix # 10.0.0.0/24 new_prefix = prefix.available_prefixes.create( {"prefix_length": 29} ) # 10.0.0.16/29 ``` """ return DetailEndpoint(self, "available-prefixes", custom_return=Prefixes) class Aggregates(Record): def __str__(self): return str(self.prefix) class Vlans(Record): def __str__(self): return super().__str__() or str(self.vid) class VlanGroups(Record): @property def available_vlans(self): """Represents the ``available-vlans`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing and creating VLANs inside a VLAN group. ## Returns DetailEndpoint object. ## Examples ```python vlan_group = nb.ipam.vlan_groups.get(1) vlan_group.available_vlans.list() # [10, 11, 12] ``` To create a new VLAN: ```python vlan_group.available_vlans.create({"name": "NewVLAN"}) # NewVLAN (10) ``` """ return DetailEndpoint(self, "available-vlans", custom_return=Vlans) class AsnRanges(Record): @property def available_asns(self): """Represents the ``available-asns`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing and creating ASNs inside an ASN range. ## Returns DetailEndpoint object. ## Examples ```python asn_range = nb.ipam.asn_ranges.get(1) asn_range.available_asns.list() # [64512, 64513, 64514] ``` To create a new ASN: ```python asn_range.available_asns.create() # 64512 ``` To create multiple ASNs: ```python asn_range.available_asns.create([{} for i in range(2)]) # [64513, 64514] ``` """ return DetailEndpoint(self, "available-asns") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/mapper.py0000644000175100017510000000764115215017101020036 0ustar00runnerrunnerfrom .circuits import ( Circuits, CircuitTerminations, ProviderNetworks, VirtualCircuits, VirtualCircuitTerminations, ) from .core import DataSources, Jobs, ObjectChanges from .dcim import ( Cables, ConsolePorts, ConsoleServerPorts, Devices, DeviceTypes, FrontPorts, Interfaces, PowerFeeds, PowerOutlets, PowerPorts, RackReservations, Racks, RearPorts, Termination, VirtualChassis, ) from .ipam import Aggregates, IpAddresses, Prefixes, VlanGroups, Vlans from .virtualization import VirtualMachineTypes, VirtualMachines from .wireless import WirelessLans CONTENT_TYPE_MAPPER = { "circuits.circuit": Circuits, "circuits.circuittermination": CircuitTerminations, "circuits.providernetwork": ProviderNetworks, "circuits.virtualcircuit": VirtualCircuits, "circuits.virtualcircuittermination": VirtualCircuitTerminations, "core.datasource": DataSources, "core.job": Jobs, "core.objectchange": ObjectChanges, "dcim.cable": Cables, "dcim.cablebundle": None, "dcim.cablepath": None, "dcim.cabletermination": Termination, "dcim.consoleport": ConsolePorts, "dcim.consoleporttemplate": None, "dcim.consoleserverport": ConsoleServerPorts, "dcim.consoleserverporttemplate": None, "dcim.device": Devices, "dcim.devicebay": None, "dcim.devicebaytemplate": None, "dcim.devicerole": None, "dcim.devicetype": DeviceTypes, "dcim.frontport": FrontPorts, "dcim.frontporttemplate": None, "dcim.interface": Interfaces, "dcim.interfacetemplate": None, "dcim.inventoryitem": None, "dcim.inventoryitemrole": None, "dcim.inventoryitemtemplate": None, "dcim.location": None, "dcim.manufacturer": None, "dcim.module": None, "dcim.modulebay": None, "dcim.modulebaytemplate": None, "dcim.moduletype": None, "dcim.platform": None, "dcim.powerfeed": PowerFeeds, "dcim.poweroutlet": PowerOutlets, "dcim.poweroutlettemplate": None, "dcim.powerpanel": None, "dcim.powerport": PowerPorts, "dcim.powerporttemplate": None, "dcim.rack": Racks, "dcim.rackgroup": None, "dcim.rackreservation": RackReservations, "dcim.rackrole": None, "dcim.rearport": RearPorts, "dcim.rearporttemplate": None, "dcim.region": None, "dcim.site": None, "dcim.sitegroup": None, "dcim.virtualchassis": VirtualChassis, "extras.configcontext": None, "extras.configrevision": None, "extras.customfield": None, "extras.customlink": None, "extras.exporttemplate": None, "extras.imageattachment": None, "extras.jobresult": None, "extras.journalentry": None, "extras.report": None, "extras.script": None, "extras.tag": None, "extras.taggeditem": None, "extras.webhook": None, "ipam.aggregate": Aggregates, "ipam.ASN": None, "ipam.FHRPgroup": None, "ipam.FHRPgroupassignment": None, "ipam.IPaddress": IpAddresses, "ipam.IPrange": None, "ipam.L2VPN": None, "ipam.L2VPNtermination": None, "ipam.prefix": Prefixes, "ipam.RIR": None, "ipam.role": None, "ipam.routetarget": None, "ipam.service": None, "ipam.servicetemplate": None, "ipam.VLAN": Vlans, "ipam.VLANgroup": VlanGroups, "ipam.VRF": None, "tenancy.contact": None, "tenancy.contactassignment": None, "tenancy.contactgroup": None, "tenancy.contactrole": None, "tenancy.tenant": None, "tenancy.tenantgroup": None, "virtualization.cluster": None, "virtualization.clustergroup": None, "virtualization.clustertype": None, "virtualization.interface": None, "virtualization.virtualmachine": VirtualMachines, "virtualization.virtualmachinetype": VirtualMachineTypes, "wireless.WirelessLAN": WirelessLans, "wireless.WirelessLANGroup": None, "wireless.wirelesslink": None, } TYPE_CONTENT_MAPPER = {v: k for k, v in CONTENT_TYPE_MAPPER.items() if v is not None} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/users.py0000644000175100017510000000136715215017101017712 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.response import JsonField, Record class Users(Record): def __str__(self): return self.username class Permissions(Record): users = [Users] constraints = JsonField ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/virtualization.py0000644000175100017510000000271115215017101021627 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.endpoint import DetailEndpoint from pynetbox.core.response import JsonField, Record from pynetbox.models.dcim import Devices from pynetbox.models.ipam import IpAddresses class VirtualMachineTypes(Record): pass class VirtualMachines(Record): device = Devices primary_ip = IpAddresses primary_ip4 = IpAddresses primary_ip6 = IpAddresses config_context = JsonField virtual_machine_type = VirtualMachineTypes @property def render_config(self): """ Represents the ``render-config`` detail endpoint. Returns a DetailEndpoint object that is the interface for viewing response from the render-config endpoint. :returns: :py:class:`.DetailEndpoint` :Examples: >>> vm = nb.virtualization.virtual_machines.get(123) >>> vm.render_config.create() """ return DetailEndpoint(self, "render-config") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/pynetbox/models/wireless.py0000644000175100017510000000124215215017101020376 0ustar00runnerrunner""" (c) 2017 DigitalOcean Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from pynetbox.core.response import Record class WirelessLans(Record): def __str__(self): return self.ssid ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1781800521.968778 pynetbox-7.8.0/pynetbox.egg-info/0000755000175100017510000000000015215017112016401 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/PKG-INFO0000644000175100017510000001225615215017111017503 0ustar00runnerrunnerMetadata-Version: 2.4 Name: pynetbox Version: 7.8.0 Summary: NetBox API client library Home-page: https://github.com/netbox-community/pynetbox Author: Zach Moody, Arthur Hanson Author-email: ahanson@netboxlabs.com License: Apache2 Keywords: netbox Classifier: Intended Audience :: Developers Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: requests<3.0,>=2.20.0 Requires-Dist: packaging Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: license-file Dynamic: requires-dist Dynamic: summary # Pynetbox Python API client library for [NetBox](https://github.com/netbox-community/netbox). > **Note:** Version 6.7 and later of the library only supports NetBox 3.3 and above. ## Compatibility Each pyNetBox Version listed below has been tested with its corresponding NetBox Version. | NetBox Version | Plugin Version | |:--------------:|:--------------:| | 4.6 | 7.8.0 | | 4.6 | 7.7.0 | | 4.5 | 7.6.1 | | 4.5 | 7.6.0 | | 4.4 | 7.5.0 | | 4.3 | 7.5.0 | | 4.2 | 7.5.0 | | 4.1 | 7.5.0 | | 4.0.6 | 7.4.1 | | 4.0.0 | 7.3.4 | | 3.7 | 7.3.0 | | 3.6 | 7.2.0 | | 3.5 | 7.1.0 | | 3.3 | 7.0.0 | ## Installation To install run `pip install pynetbox`. Alternatively, you can clone the repo and run `python setup.py install`. ## Quick Start The full pynetbox API is documented on [GitHub Pages](https://netbox-community.github.io/pynetbox/), but the following should be enough to get started using it. To begin, import pynetbox and instantiate the API. ``` import pynetbox nb = pynetbox.api( 'http://localhost:8000', token='d6f4e314a5b5fefd164995169f28ae32d987704f' ) ``` The first argument the .api() method takes is the NetBox URL. There are a handful of named arguments you can provide, but in most cases none are required to simply pull data. In order to write, the `token` argument should to be provided. ## Queries The pynetbox API is setup so that NetBox's apps are attributes of the `.api()` object, and in turn those apps have attribute representing each endpoint. Each endpoint has a handful of methods available to carry out actions on the endpoint. For example, in order to query all the objects in the `devices` endpoint you would do the following: ``` >>> devices = nb.dcim.devices.all() >>> for device in devices: ... print(device.name) ... test1-leaf1 test1-leaf2 test1-leaf3 >>> ``` Note that the all() and filter() methods are generators and return an object that can be iterated over only once. If you are going to be iterating over it repeatedly you need to either call the all() method again, or encapsulate the results in a `list` object like this: ``` >>> devices = list(nb.dcim.devices.all()) ``` ### Threading pynetbox supports multithreaded calls for `.filter()` and `.all()` queries. It is **highly recommended** you have `MAX_PAGE_SIZE` in your Netbox install set to anything *except* `0` or `None`. The default value of `1000` is usually a good value to use. To enable threading, add `threading=True` parameter to the `.api`: ```python nb = pynetbox.api( 'http://localhost:8000', threading=True, ) ``` ### Filters validation NetBox doesn't validate filters passed to the GET API endpoints, which are accessed with `.get()` and `.filter()`. If a filter is incorrect, NetBox silently returns the entire database table content. Pynetbox allows to check provided parameters against NetBox OpenAPI specification before doing the call, and raise an exception if a parameter is incorrect. This can be enabled globally by setting `strict_filters=True` in the API object initialization: ```python nb = pynetbox.api( 'http://localhost:8000', strict_filters=True, ) ``` This can also be enabled and disabled on a per-request basis: ```python # Disable for one request when enabled globally. # Will not raise an exception and return the entire Device table. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=False) # Enable for one request when not enabled globally. # Will raise an exception. nb.dcim.devices.filter(non_existing_filter="aaaa", strict_filters=True) ``` ## Running Tests First, create and activate a Python virtual environment in the pynetbox directory to isolate the project dependencies: ```python python3 -m venv venv source venv/bin/activate ``` Install both requirements files: ```python pip install -r requirements.txt pip install -r requirements-dev.txt ``` The test suite requires Docker to be installed and running, as it will download and launch netbox-docker containers during test execution. With Docker installed and running, execute the following command to run the test suite: ```python pytest ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/SOURCES.txt0000644000175100017510000001621615215017111020272 0ustar00runnerrunner.gitignore .pre-commit-config.yaml .readthedocs.yaml AGENTS.md CHANGELOG.md CLAUDE.md LICENSE README.md mkdocs.yml requirements-dev.txt requirements.txt setup.py .claude/README.md .claude/skills/run-tests/SKILL.md .github/PULL_REQUEST_TEMPLATE.md .github/ISSUE_TEMPLATE/bug_report.yaml .github/ISSUE_TEMPLATE/config.yml .github/ISSUE_TEMPLATE/deprecation.yaml .github/ISSUE_TEMPLATE/documentation_change.yaml .github/ISSUE_TEMPLATE/feature_request.yaml .github/ISSUE_TEMPLATE/housekeeping.yaml .github/workflows/build-mkdocs.yml .github/workflows/claude.yml .github/workflows/no-blank-issue.yml .github/workflows/publish.yml .github/workflows/py3.yml docs/advanced.md docs/api.md docs/branching.md docs/circuits.md docs/core.md docs/custom-objects.md docs/dcim.md docs/endpoint.md docs/exceptions.md docs/extensions.md docs/getting-started.md docs/index.md docs/installation.md docs/ipam.md docs/release-notes.md docs/response.md docs/virtualization.md docs/development/getting-started.md docs/development/index.md docs/development/release-checklist.md pynetbox/__init__.py pynetbox.egg-info/PKG-INFO pynetbox.egg-info/SOURCES.txt pynetbox.egg-info/dependency_links.txt pynetbox.egg-info/not-zip-safe pynetbox.egg-info/requires.txt pynetbox.egg-info/top_level.txt pynetbox/core/__init__.py pynetbox/core/api.py pynetbox/core/app.py pynetbox/core/endpoint.py pynetbox/core/extension.py pynetbox/core/query.py pynetbox/core/response.py pynetbox/core/util.py pynetbox/extensions/__init__.py pynetbox/extensions/branching.py pynetbox/extensions/custom_objects.py pynetbox/models/__init__.py pynetbox/models/circuits.py pynetbox/models/core.py pynetbox/models/dcim.py pynetbox/models/extras.py pynetbox/models/ipam.py pynetbox/models/mapper.py pynetbox/models/users.py pynetbox/models/virtualization.py pynetbox/models/wireless.py tests/__init__.py tests/conftest.py tests/test_api.py tests/test_app.py tests/test_circuits.py tests/test_dcim.py tests/test_tenancy.py tests/test_users.py tests/test_virtualization.py tests/test_wireless.py tests/util.py tests/fixtures/api/token_provision_v1_legacy.json tests/fixtures/api/token_provision_v1_with_token.json tests/fixtures/api/token_provision_v2.json tests/fixtures/circuits/circuit.json tests/fixtures/circuits/circuit_termination.json tests/fixtures/circuits/circuit_terminations.json tests/fixtures/circuits/circuit_type.json tests/fixtures/circuits/circuit_types.json tests/fixtures/circuits/circuits.json tests/fixtures/circuits/provider.json tests/fixtures/circuits/providers.json tests/fixtures/dcim/cable.json tests/fixtures/dcim/cables.json tests/fixtures/dcim/choices.json tests/fixtures/dcim/console_port.json tests/fixtures/dcim/console_port_template.json tests/fixtures/dcim/console_port_templates.json tests/fixtures/dcim/console_ports.json tests/fixtures/dcim/console_server_port.json tests/fixtures/dcim/console_server_port_template.json tests/fixtures/dcim/console_server_port_templates.json tests/fixtures/dcim/console_server_ports.json tests/fixtures/dcim/device.json tests/fixtures/dcim/device_bay.json tests/fixtures/dcim/device_bay_template.json tests/fixtures/dcim/device_bay_templates.json tests/fixtures/dcim/device_bays.json tests/fixtures/dcim/device_bulk_create.json tests/fixtures/dcim/device_role.json tests/fixtures/dcim/device_roles.json tests/fixtures/dcim/device_type.json tests/fixtures/dcim/device_types.json tests/fixtures/dcim/devices.json tests/fixtures/dcim/interface.json tests/fixtures/dcim/interface_connection.json tests/fixtures/dcim/interface_connections.json tests/fixtures/dcim/interface_template.json tests/fixtures/dcim/interface_templates.json tests/fixtures/dcim/interface_trace.json tests/fixtures/dcim/interfaces.json tests/fixtures/dcim/interfaces_1.json tests/fixtures/dcim/interfaces_2.json tests/fixtures/dcim/inventory_item.json tests/fixtures/dcim/inventory_items.json tests/fixtures/dcim/manufacturer.json tests/fixtures/dcim/manufacturers.json tests/fixtures/dcim/napalm.json tests/fixtures/dcim/platform.json tests/fixtures/dcim/platforms.json tests/fixtures/dcim/power_outlet.json tests/fixtures/dcim/power_outlet_template.json tests/fixtures/dcim/power_outlet_templates.json tests/fixtures/dcim/power_outlets.json tests/fixtures/dcim/power_port.json tests/fixtures/dcim/power_port_template.json tests/fixtures/dcim/power_port_templates.json tests/fixtures/dcim/power_ports.json tests/fixtures/dcim/rack.json tests/fixtures/dcim/rack_group.json tests/fixtures/dcim/rack_groups.json tests/fixtures/dcim/rack_reservation.json tests/fixtures/dcim/rack_reservations.json tests/fixtures/dcim/rack_role.json tests/fixtures/dcim/rack_roles.json tests/fixtures/dcim/rack_u.json tests/fixtures/dcim/racks.json tests/fixtures/dcim/region.json tests/fixtures/dcim/regions.json tests/fixtures/dcim/site.json tests/fixtures/dcim/sites.json tests/fixtures/dcim/virtual_chassis_device.json tests/fixtures/dcim/virtual_chassis_devices.json tests/fixtures/ipam/aggregate.json tests/fixtures/ipam/aggregates.json tests/fixtures/ipam/available-ips-post.json tests/fixtures/ipam/available-ips.json tests/fixtures/ipam/available-prefixes-post.json tests/fixtures/ipam/available-prefixes.json tests/fixtures/ipam/ip_address.json tests/fixtures/ipam/ip_addresses.json tests/fixtures/ipam/prefix.json tests/fixtures/ipam/prefixes.json tests/fixtures/ipam/rir.json tests/fixtures/ipam/rirs.json tests/fixtures/ipam/role.json tests/fixtures/ipam/roles.json tests/fixtures/ipam/vlan.json tests/fixtures/ipam/vlan_group.json tests/fixtures/ipam/vlan_groups.json tests/fixtures/ipam/vlans.json tests/fixtures/ipam/vrf.json tests/fixtures/ipam/vrfs.json tests/fixtures/tenancy/tenant.json tests/fixtures/tenancy/tenant_group.json tests/fixtures/tenancy/tenant_groups.json tests/fixtures/tenancy/tenants.json tests/fixtures/users/group.json tests/fixtures/users/groups.json tests/fixtures/users/permission.json tests/fixtures/users/permissions.json tests/fixtures/users/unknown_model.json tests/fixtures/users/user.json tests/fixtures/users/users.json tests/fixtures/virtualization/cluster.json tests/fixtures/virtualization/cluster_group.json tests/fixtures/virtualization/cluster_groups.json tests/fixtures/virtualization/cluster_type.json tests/fixtures/virtualization/cluster_types.json tests/fixtures/virtualization/clusters.json tests/fixtures/virtualization/interface.json tests/fixtures/virtualization/interfaces.json tests/fixtures/virtualization/virtual_machine.json tests/fixtures/virtualization/virtual_machine_type.json tests/fixtures/virtualization/virtual_machine_types.json tests/fixtures/virtualization/virtual_machines.json tests/fixtures/wireless/wireless_lan.json tests/fixtures/wireless/wireless_lans.json tests/integration/conftest.py tests/integration/test_circuits.py tests/integration/test_dcim.py tests/integration/test_ipam.py tests/integration/test_pagination.py tests/unit/__init__.py tests/unit/test_branching_extension.py tests/unit/test_custom_objects_extension.py tests/unit/test_detailendpoint.py tests/unit/test_endpoint.py tests/unit/test_endpoint_strict_filter.py tests/unit/test_extensions.py tests/unit/test_extras.py tests/unit/test_file_upload.py tests/unit/test_mapper.py tests/unit/test_multiformat_endpoint.py tests/unit/test_query.py tests/unit/test_request.py tests/unit/test_response.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/dependency_links.txt0000644000175100017510000000000115215017111022446 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/not-zip-safe0000644000175100017510000000000115215017111020626 0ustar00runnerrunner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/requires.txt0000644000175100017510000000004015215017111020772 0ustar00runnerrunnerrequests<3.0,>=2.20.0 packaging ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800521.0 pynetbox-7.8.0/pynetbox.egg-info/top_level.txt0000644000175100017510000000001115215017111021122 0ustar00runnerrunnerpynetbox ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/requirements-dev.txt0000644000175100017510000000015715215017101017100 0ustar00runnerrunnermkdocs-material==9.6.14 mkdocstrings[python]==0.29.1 pymdown-extensions>=10.0 pytest pytest-docker PyYAML ruff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/requirements.txt0000644000175100017510000000006215215017101016317 0ustar00runnerrunnerrequests>=2.32.3,<3.0 urllib3>=2.2.3,<3 packaging ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9697094 pynetbox-7.8.0/setup.cfg0000644000175100017510000000004615215017112014660 0ustar00runnerrunner[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/setup.py0000644000175100017510000000170015215017101014545 0ustar00runnerrunnerfrom setuptools import find_packages, setup setup( name="pynetbox", description="NetBox API client library", url="https://github.com/netbox-community/pynetbox", author="Zach Moody, Arthur Hanson", author_email="ahanson@netboxlabs.com", license="Apache2", include_package_data=True, use_scm_version=True, setup_requires=["setuptools_scm"], packages=find_packages(exclude=["tests", "tests.*"]), long_description=open("README.md").read(), long_description_content_type="text/markdown", install_requires=["requests>=2.20.0,<3.0", "packaging"], zip_safe=False, keywords=["netbox"], classifiers=[ "Intended Audience :: Developers", "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ], ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1781800521.950734 pynetbox-7.8.0/tests/0000755000175100017510000000000015215017112014201 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/__init__.py0000644000175100017510000000000015215017101016276 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/conftest.py0000644000175100017510000000434115215017101016400 0ustar00runnerrunnerfrom urllib import parse import pytest from packaging import version DEFAULT_NETBOX_VERSIONS = "4.4" def pytest_addoption(parser): """Hook on the pytest option parser setup. Add some extra options to the parser. """ parser.addoption( "--netbox-versions", action="store", default=DEFAULT_NETBOX_VERSIONS, help=( "The versions of netbox to run integration tests against, as a" " comma-separated list. Default: %s" % DEFAULT_NETBOX_VERSIONS ), ) parser.addoption( "--no-cleanup", dest="cleanup", action="store_false", help=( "Skip any cleanup steps after the pytest session finishes. Any containers" " created will be left running and the docker-compose files used to" " create them will be left on disk." ), ) parser.addoption( "--url-override", dest="url_override", action="store", help=( "Overrides the URL to run tests to. This allows for testing to the same" " containers for separate runs." ), ) def pytest_configure(config): """Hook that runs after test collection is completed. Here we can modify items in the collected tests or parser args. """ # verify the netbox versions parse correctly and split them config.option.netbox_versions = [ version.Version(version_string) for version_string in config.option.netbox_versions.split(",") ] if "no:docker" in config.option.plugins and config.option.url_override: url_parse = parse.urlparse(config.option.url_override) class DockerServicesMock: def __init__(self, ports): self.ports = ports def wait_until_responsive(self, *args, **kwargs): return None def port_for(self, *args): return self.ports class Plugin: @pytest.fixture(scope="session") def docker_ip(self): return "127.0.0.1" @pytest.fixture(scope="session") def docker_services(self): return DockerServicesMock(url_parse.port) config.pluginmanager.register(Plugin()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9402857 pynetbox-7.8.0/tests/fixtures/0000755000175100017510000000000015215017112016052 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9511356 pynetbox-7.8.0/tests/fixtures/api/0000755000175100017510000000000015215017112016623 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/api/token_provision_v1_legacy.json0000644000175100017510000000007215215017101024675 0ustar00runnerrunner{ "key": "1234567890123456789012345678901234567890" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/api/token_provision_v1_with_token.json0000644000175100017510000000022215215017101025601 0ustar00runnerrunner{ "id": 1, "version": 1, "key": "1234567890123456789012345678901234567890", "token": "plaintexttoken7890abcdef1234567890abcdef" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/api/token_provision_v2.json0000644000175100017510000000017115215017101023352 0ustar00runnerrunner{ "id": 1, "version": 2, "key": "shortkey1234567", "token": "plaintexttoken7890abcdef1234567890abcdef" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9518905 pynetbox-7.8.0/tests/fixtures/circuits/0000755000175100017510000000000015215017112017677 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuit.json0000644000175100017510000000075315215017101022237 0ustar00runnerrunner{ "id": 1, "cid": "123456", "provider": { "id": 1, "url": "http://localhost:8000/api/circuits/providers/1/", "name": "TEST", "slug": "test" }, "type": { "id": 1, "url": "http://localhost:8000/api/circuits/circuit-types/1/", "name": "Transit", "slug": "transit" }, "tenant": null, "install_date": null, "commit_rate": null, "description": "", "comments": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuit_termination.json0000644000175100017510000000166015215017101024646 0ustar00runnerrunner{ "id": 1, "circuit": { "id": 1, "url": "http://localhost:8000/api/circuits/circuits/1/", "cid": "123456" }, "term_side": "A", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "interface": { "id": 4, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "TEST", "connection": null, "connected_interface": null }, "port_speed": 1000000, "upstream_speed": null, "xconnect_id": "", "pp_info": "" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuit_terminations.json0000644000175100017510000000247315215017101025034 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "circuit": { "id": 1, "url": "http://localhost:8000/api/circuits/circuits/1/", "cid": "123456" }, "term_side": "A", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "interface": { "id": 4, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "TEST", "connection": null, "connected_interface": null }, "port_speed": 1000000, "upstream_speed": null, "xconnect_id": "", "pp_info": "" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuit_type.json0000644000175100017510000000007515215017101023275 0ustar00runnerrunner{ "id": 1, "name": "Transit", "slug": "transit" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuit_types.json0000644000175100017510000000027015215017101023455 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "Transit", "slug": "transit" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/circuits.json0000644000175100017510000000135615215017101022422 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "cid": "123456", "provider": { "id": 1, "url": "http://localhost:8000/api/circuits/providers/1/", "name": "TEST", "slug": "test" }, "type": { "id": 1, "url": "http://localhost:8000/api/circuits/circuit-types/1/", "name": "Transit", "slug": "transit" }, "tenant": null, "install_date": null, "commit_rate": null, "description": "", "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/provider.json0000644000175100017510000000031615215017101022422 0ustar00runnerrunner{ "id": 1, "name": "TEST", "slug": "test", "asn": null, "account": "", "portal_url": "", "noc_contact": "", "admin_contact": "", "comments": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/circuits/providers.json0000644000175100017510000000060115215017101022602 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST", "slug": "test", "asn": null, "account": "", "portal_url": "", "noc_contact": "", "admin_contact": "", "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9597864 pynetbox-7.8.0/tests/fixtures/dcim/0000755000175100017510000000000015215017112016766 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/cable.json0000644000175100017510000000202615215017101020725 0ustar00runnerrunner{ "id": 1, "termination_a_type": "dcim.consoleport", "termination_a_id": 1, "termination_a": { "id": 1, "url": "http://localhost:8000/api/dcim/console-ports/1/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "tst1-test1", "display_name": "tst1-test1" }, "name": "Console", "cable": 1 }, "termination_b_type": "dcim.consoleserverport", "termination_b_id": 2, "termination_b": { "id": 2, "url": "http://localhost:8000/api/dcim/console-server-ports/2/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "tst1-test2", "display_name": "tst1-test2" }, "name": "Port 10", "cable": 1 }, "type": null, "status": { "value": true, "label": "Connected" }, "label": "", "color": "", "length": null, "length_unit": null }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/cables.json0000644000175100017510000001013015215017101021103 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "termination_a_type": "dcim.consoleport", "termination_a_id": 1, "termination_a": { "id": 1, "url": "http://localhost:8000/api/dcim/console-ports/1/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "tst1-test1", "display_name": "tst1-test1" }, "name": "Console", "cable": 1 }, "termination_b_type": "dcim.consoleserverport", "termination_b_id": 2, "termination_b": { "id": 2, "url": "http://localhost:8000/api/dcim/console-server-ports/2/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "tst1-test2", "display_name": "tst1-test2" }, "name": "Port 10", "cable": 1 }, "type": null, "status": { "value": true, "label": "Connected" }, "label": "", "color": "", "length": null, "length_unit": null }, { "id": 2, "termination_a_type": "dcim.consoleport", "termination_a_id": 3, "termination_a": { "id": 3, "url": "http://localhost:8000/api/dcim/console-ports/3/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "tst1-test3", "display_name": "tst1-test3" }, "name": "Console", "cable": 2 }, "termination_b_type": "dcim.consoleserverport", "termination_b_id": 4, "termination_b": { "id": 4, "url": "http://localhost:8000/api/dcim/console-server-ports/4/", "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "tst1-test4", "display_name": "tst1-test4" }, "name": "Port 11", "cable": 2 }, "type": null, "status": { "value": true, "label": "Connected" }, "label": "", "color": "", "length": null, "length_unit": null }, { "id": 3, "termination_a_type": "dcim.consoleport", "termination_a_id": 5, "termination_a": { "id": 5, "url": "http://localhost:8000/api/dcim/console-ports/5/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "tst1-test5", "display_name": "tst1-test5" }, "name": "Console", "cable": 3 }, "termination_b_type": "dcim.consoleserverport", "termination_b_id": 6, "termination_b": { "id": 6, "url": "http://localhost:8000/api/dcim/console-server-ports/6/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "tst1-test6", "display_name": "tst1-test6" }, "name": "Port 1", "cable": 3 }, "type": null, "status": { "value": true, "label": "Connected" }, "label": "", "color": "", "length": null, "length_unit": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/choices.json0000644000175100017510000002330315215017101021275 0ustar00runnerrunner{ "device:face": [ { "label": "Front", "value": 0 }, { "label": "Rear", "value": 1 } ], "device:status": [ { "label": "Active", "value": 1 }, { "label": "Offline", "value": 0 }, { "label": "Planned", "value": 2 }, { "label": "Staged", "value": 3 }, { "label": "Failed", "value": 4 }, { "label": "Inventory", "value": 5 } ], "console-port:connection_status": [ { "label": "Planned", "value": false }, { "label": "Connected", "value": true } ], "interface:form_factor": [ { "label": "Virtual", "value": 0 }, { "label": "Link Aggregation Group (LAG)", "value": 200 }, { "label": "100BASE-TX (10/100ME)", "value": 800 }, { "label": "1000BASE-T (1GE)", "value": 1000 }, { "label": "10GBASE-T (10GE)", "value": 1150 }, { "label": "10GBASE-CX4 (10GE)", "value": 1170 }, { "label": "GBIC (1GE)", "value": 1050 }, { "label": "SFP (1GE)", "value": 1100 }, { "label": "SFP+ (10GE)", "value": 1200 }, { "label": "XFP (10GE)", "value": 1300 }, { "label": "XENPAK (10GE)", "value": 1310 }, { "label": "X2 (10GE)", "value": 1320 }, { "label": "SFP28 (25GE)", "value": 1350 }, { "label": "QSFP+ (40GE)", "value": 1400 }, { "label": "CFP (100GE)", "value": 1500 }, { "label": "CFP2 (100GE)", "value": 1510 }, { "label": "CFP4 (100GE)", "value": 1520 }, { "label": "Cisco CPAK (100GE)", "value": 1550 }, { "label": "QSFP28 (100GE)", "value": 1600 }, { "label": "IEEE 802.11a", "value": 2600 }, { "label": "IEEE 802.11b/g", "value": 2610 }, { "label": "IEEE 802.11n", "value": 2620 }, { "label": "IEEE 802.11ac", "value": 2630 }, { "label": "IEEE 802.11ad", "value": 2640 }, { "label": "SFP (1GFC)", "value": 3010 }, { "label": "SFP (2GFC)", "value": 3020 }, { "label": "SFP (4GFC)", "value": 3040 }, { "label": "SFP+ (8GFC)", "value": 3080 }, { "label": "SFP+ (16GFC)", "value": 3160 }, { "label": "T1 (1.544 Mbps)", "value": 4000 }, { "label": "E1 (2.048 Mbps)", "value": 4010 }, { "label": "T3 (45 Mbps)", "value": 4040 }, { "label": "E3 (34 Mbps)", "value": 4050 }, { "label": "Cisco StackWise", "value": 5000 }, { "label": "Cisco StackWise Plus", "value": 5050 }, { "label": "Cisco FlexStack", "value": 5100 }, { "label": "Cisco FlexStack Plus", "value": 5150 }, { "label": "Juniper VCP", "value": 5200 }, { "label": "Extreme SummitStack", "value": 5300 }, { "label": "Extreme SummitStack-128", "value": 5310 }, { "label": "Extreme SummitStack-256", "value": 5320 }, { "label": "Extreme SummitStack-512", "value": 5330 }, { "label": "Other", "value": 32767 } ], "interface:mode": [ { "label": "Access", "value": 100 }, { "label": "Tagged", "value": 200 }, { "label": "Tagged All", "value": 300 } ], "interface-connection:connection_status": [ { "label": "Planned", "value": false }, { "label": "Connected", "value": true } ], "interface-template:form_factor": [ { "label": "Virtual", "value": 0 }, { "label": "Link Aggregation Group (LAG)", "value": 200 }, { "label": "100BASE-TX (10/100ME)", "value": 800 }, { "label": "1000BASE-T (1GE)", "value": 1000 }, { "label": "10GBASE-T (10GE)", "value": 1150 }, { "label": "10GBASE-CX4 (10GE)", "value": 1170 }, { "label": "GBIC (1GE)", "value": 1050 }, { "label": "SFP (1GE)", "value": 1100 }, { "label": "SFP+ (10GE)", "value": 1200 }, { "label": "XFP (10GE)", "value": 1300 }, { "label": "XENPAK (10GE)", "value": 1310 }, { "label": "X2 (10GE)", "value": 1320 }, { "label": "SFP28 (25GE)", "value": 1350 }, { "label": "QSFP+ (40GE)", "value": 1400 }, { "label": "CFP (100GE)", "value": 1500 }, { "label": "CFP2 (100GE)", "value": 1510 }, { "label": "CFP4 (100GE)", "value": 1520 }, { "label": "Cisco CPAK (100GE)", "value": 1550 }, { "label": "QSFP28 (100GE)", "value": 1600 }, { "label": "IEEE 802.11a", "value": 2600 }, { "label": "IEEE 802.11b/g", "value": 2610 }, { "label": "IEEE 802.11n", "value": 2620 }, { "label": "IEEE 802.11ac", "value": 2630 }, { "label": "IEEE 802.11ad", "value": 2640 }, { "label": "SFP (1GFC)", "value": 3010 }, { "label": "SFP (2GFC)", "value": 3020 }, { "label": "SFP (4GFC)", "value": 3040 }, { "label": "SFP+ (8GFC)", "value": 3080 }, { "label": "SFP+ (16GFC)", "value": 3160 }, { "label": "T1 (1.544 Mbps)", "value": 4000 }, { "label": "E1 (2.048 Mbps)", "value": 4010 }, { "label": "T3 (45 Mbps)", "value": 4040 }, { "label": "E3 (34 Mbps)", "value": 4050 }, { "label": "Cisco StackWise", "value": 5000 }, { "label": "Cisco StackWise Plus", "value": 5050 }, { "label": "Cisco FlexStack", "value": 5100 }, { "label": "Cisco FlexStack Plus", "value": 5150 }, { "label": "Juniper VCP", "value": 5200 }, { "label": "Extreme SummitStack", "value": 5300 }, { "label": "Extreme SummitStack-128", "value": 5310 }, { "label": "Extreme SummitStack-256", "value": 5320 }, { "label": "Extreme SummitStack-512", "value": 5330 }, { "label": "Other", "value": 32767 } ], "power-port:connection_status": [ { "label": "Planned", "value": false }, { "label": "Connected", "value": true } ], "rack:type": [ { "label": "2-post frame", "value": 100 }, { "label": "4-post frame", "value": 200 }, { "label": "4-post cabinet", "value": 300 }, { "label": "Wall-mounted frame", "value": 1000 }, { "label": "Wall-mounted cabinet", "value": 1100 } ], "rack:width": [ { "label": "19 inches", "value": 19 }, { "label": "23 inches", "value": 23 } ], "site:status": [ { "label": "Active", "value": 1 }, { "label": "Planned", "value": 2 }, { "label": "Retired", "value": 4 } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_port.json0000644000175100017510000000102615215017101022364 0ustar00runnerrunner{ "id": 1, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "Console (RE0)", "cs_port": { "id": 27, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 3", "connected_console": 1 }, "connection_status": true }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_port_template.json0000644000175100017510000000061115215017101024256 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "Console (RE0)" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_port_templates.json0000644000175100017510000000726715215017101024457 0ustar00runnerrunner{ "count": 7, "next": null, "previous": null, "results": [ { "id": 3, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "Console (RE0)" }, { "id": 4, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "Console (RE1)" }, { "id": 1, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "Console (RE0)" }, { "id": 2, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "Console (RE1)" }, { "id": 5, "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "name": "Console" }, { "id": 6, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Console" }, { "id": 7, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "Serial" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_ports.json0000644000175100017510000001664015215017101022557 0ustar00runnerrunner{ "count": 15, "next": null, "previous": null, "results": [ { "id": 3, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "Console (RE0)", "cs_port": { "id": 5, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 1", "connected_console": 3 }, "connection_status": true }, { "id": 4, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "Console (RE1)", "cs_port": { "id": 16, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 2", "connected_console": 4 }, "connection_status": true }, { "id": 11, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "Console (RE0)", "cs_port": null, "connection_status": true }, { "id": 12, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "Console (RE1)", "cs_port": null, "connection_status": true }, { "id": 1, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "Console (RE0)", "cs_port": { "id": 27, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 3", "connected_console": 1 }, "connection_status": true }, { "id": 2, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "Console (RE1)", "cs_port": { "id": 38, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 4", "connected_console": 2 }, "connection_status": true }, { "id": 9, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "Console (RE0)", "cs_port": null, "connection_status": true }, { "id": 10, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "Console (RE1)", "cs_port": null, "connection_status": true }, { "id": 6, "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "Console", "cs_port": { "id": 48, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 5", "connected_console": 6 }, "connection_status": true }, { "id": 7, "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "Console", "cs_port": null, "connection_status": true }, { "id": 13, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Console", "cs_port": null, "connection_status": true }, { "id": 15, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "Serial", "cs_port": null, "connection_status": true }, { "id": 16, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "Serial", "cs_port": null, "connection_status": true }, { "id": 5, "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "Console", "cs_port": { "id": 49, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 6", "connected_console": 5 }, "connection_status": true }, { "id": 8, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "Console", "cs_port": null, "connection_status": true } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_server_port.json0000644000175100017510000000035015215017101023751 0ustar00runnerrunner{ "id": 1, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 1", "connected_console": 3 }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_server_port_template.json0000644000175100017510000000061715215017101025652 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "name": "Console" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_server_port_templates.json0000644000175100017510000006310515215017101026036 0ustar00runnerrunner{ "count": 50, "next": null, "previous": null, "results": [ { "id": 1, "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "name": "Console" }, { "id": 3, "device_type": { "id": 4, "url": "http://localhost:8000/api/dcim/device-types/4/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s" }, "name": "Console" }, { "id": 4, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 1" }, { "id": 13, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 10" }, { "id": 14, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 11" }, { "id": 15, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 12" }, { "id": 16, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 13" }, { "id": 17, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 14" }, { "id": 18, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 15" }, { "id": 19, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 16" }, { "id": 20, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 17" }, { "id": 21, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 18" }, { "id": 22, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 19" }, { "id": 5, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 2" }, { "id": 23, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 20" }, { "id": 24, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 21" }, { "id": 25, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 22" }, { "id": 26, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 23" }, { "id": 27, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 24" }, { "id": 28, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 25" }, { "id": 29, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 26" }, { "id": 30, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 27" }, { "id": 31, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 28" }, { "id": 32, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 29" }, { "id": 6, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 3" }, { "id": 33, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 30" }, { "id": 34, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 31" }, { "id": 35, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 32" }, { "id": 36, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 33" }, { "id": 37, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 34" }, { "id": 38, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 35" }, { "id": 39, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 36" }, { "id": 40, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 37" }, { "id": 41, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 38" }, { "id": 42, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 39" }, { "id": 7, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 4" }, { "id": 43, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 40" }, { "id": 44, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 41" }, { "id": 45, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 42" }, { "id": 46, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 43" }, { "id": 47, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 44" }, { "id": 48, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 45" }, { "id": 49, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 46" }, { "id": 50, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 47" }, { "id": 51, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 48" }, { "id": 8, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 5" }, { "id": 9, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 6" }, { "id": 10, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 7" }, { "id": 11, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 8" }, { "id": 12, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "Port 9" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/console_server_ports.json0000644000175100017510000003660215215017101024145 0ustar00runnerrunner{ "count": 48, "next": null, "previous": null, "results": [ { "id": 5, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 1", "connected_console": 3 }, { "id": 16, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 2", "connected_console": 4 }, { "id": 27, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 3", "connected_console": 1 }, { "id": 38, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 4", "connected_console": 2 }, { "id": 48, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 5", "connected_console": 6 }, { "id": 49, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 6", "connected_console": 5 }, { "id": 50, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 7", "connected_console": null }, { "id": 51, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 8", "connected_console": null }, { "id": 52, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 9", "connected_console": null }, { "id": 6, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 10", "connected_console": null }, { "id": 7, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 11", "connected_console": null }, { "id": 8, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 12", "connected_console": null }, { "id": 9, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 13", "connected_console": null }, { "id": 10, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 14", "connected_console": null }, { "id": 11, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 15", "connected_console": null }, { "id": 12, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 16", "connected_console": null }, { "id": 13, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 17", "connected_console": null }, { "id": 14, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 18", "connected_console": null }, { "id": 15, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 19", "connected_console": null }, { "id": 17, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 20", "connected_console": null }, { "id": 18, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 21", "connected_console": null }, { "id": 19, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 22", "connected_console": null }, { "id": 20, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 23", "connected_console": null }, { "id": 21, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 24", "connected_console": null }, { "id": 22, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 25", "connected_console": null }, { "id": 23, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 26", "connected_console": null }, { "id": 24, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 27", "connected_console": null }, { "id": 25, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 28", "connected_console": null }, { "id": 26, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 29", "connected_console": null }, { "id": 28, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 30", "connected_console": null }, { "id": 29, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 31", "connected_console": null }, { "id": 30, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 32", "connected_console": null }, { "id": 31, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 33", "connected_console": null }, { "id": 32, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 34", "connected_console": null }, { "id": 33, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 35", "connected_console": null }, { "id": 34, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 36", "connected_console": null }, { "id": 35, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 37", "connected_console": null }, { "id": 36, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 38", "connected_console": null }, { "id": 37, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 39", "connected_console": null }, { "id": 39, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 40", "connected_console": null }, { "id": 40, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 41", "connected_console": null }, { "id": 41, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 42", "connected_console": null }, { "id": 42, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 43", "connected_console": null }, { "id": 43, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 44", "connected_console": null }, { "id": 44, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 45", "connected_console": null }, { "id": 45, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 46", "connected_console": null }, { "id": 46, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 47", "connected_console": null }, { "id": 47, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "Port 48", "connected_console": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device.json0000644000175100017510000000371215215017101021121 0ustar00runnerrunner{ "id": 1, "name": "test1-edge1", "display_name": "test1-edge1", "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "role": { "id": 1, "url": "http://localhost:8000/api/dcim/device-roles/1/", "name": "Router", "slug": "router" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "5555555555", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 1, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": { "id": 1, "url": "http://localhost:8000/api/ipam/ip-addresses/1/", "family": 4, "address": "10.0.255.1/32" }, "primary_ip4": { "id": 1, "url": "http://localhost:8000/api/ipam/ip-addresses/1/", "family": 4, "address": "10.0.255.1/32" }, "primary_ip6": null, "oob_ip": { "id": 5, "url": "http://localhost:8000/api/ipam/ip-addresses/5/", "family": 4, "address": "192.0.2.1/32" }, "comments": "", "local_context_data": { "testing": "test" }, "custom_fields": {}, "config_context": { "test_key": "test_val" } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_bay.json0000644000175100017510000000037115215017101021752 0ustar00runnerrunner{ "id": 1, "device": { "id": 13, "url": "http://localhost:8000/api/dcim/devices/13/", "name": "test1-mothership", "display_name": "test1-mothership" }, "name": "thing-1", "installed_device": null }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_bay_template.json0000644000175100017510000000062315215017101023645 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 7, "url": "http://localhost:8000/api/dcim/device-types/7/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "Mothership", "slug": "mothership" }, "name": "thing-1" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_bay_templates.json0000644000175100017510000000322015215017101024024 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "device_type": { "id": 7, "url": "http://localhost:8000/api/dcim/device-types/7/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "Mothership", "slug": "mothership" }, "name": "thing-1" }, { "id": 2, "device_type": { "id": 7, "url": "http://localhost:8000/api/dcim/device-types/7/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "Mothership", "slug": "mothership" }, "name": "thing-2" }, { "id": 3, "device_type": { "id": 7, "url": "http://localhost:8000/api/dcim/device-types/7/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "Mothership", "slug": "mothership" }, "name": "thing-3" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_bays.json0000644000175100017510000000211215215017101022130 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "device": { "id": 13, "url": "http://localhost:8000/api/dcim/devices/13/", "name": "test1-mothership", "display_name": "test1-mothership" }, "name": "thing-1", "installed_device": null }, { "id": 2, "device": { "id": 13, "url": "http://localhost:8000/api/dcim/devices/13/", "name": "test1-mothership", "display_name": "test1-mothership" }, "name": "thing-2", "installed_device": null }, { "id": 3, "device": { "id": 13, "url": "http://localhost:8000/api/dcim/devices/13/", "name": "test1-mothership", "display_name": "test1-mothership" }, "name": "thing-3", "installed_device": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_bulk_create.json0000644000175100017510000000151415215017101023457 0ustar00runnerrunner[ { "id": 1, "name": "test1-core3", "device_type": 1, "role": 3, "tenant": null, "platform": null, "serial": "", "asset_tag": null, "site": 1, "rack": null, "position": null, "face": null, "status": 1, "primary_ip4": null, "primary_ip6": null, "cluster": null, "comments": "" }, { "id": 2, "name": "test1-core4", "device_type": 1, "role": 3, "tenant": null, "platform": null, "serial": "", "asset_tag": null, "site": 1, "rack": null, "position": null, "face": null, "status": 1, "primary_ip4": null, "primary_ip6": null, "cluster": null, "comments": "" } ]././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_role.json0000644000175100017510000000012215215017101022132 0ustar00runnerrunner{ "id": 1, "name": "Router", "slug": "router", "color": "purple" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_roles.json0000644000175100017510000000160515215017101022324 0ustar00runnerrunner{ "count": 6, "next": null, "previous": null, "results": [ { "id": 3, "name": "Core Switch", "slug": "core-switch", "color": "red" }, { "id": 4, "name": "Leaf Switch", "slug": "leaf-switch", "color": "teal" }, { "id": 5, "name": "OOB Switch", "slug": "oob-switch", "color": "purple" }, { "id": 6, "name": "PDU", "slug": "pdu", "color": "yellow" }, { "id": 1, "name": "Router", "slug": "router", "color": "purple" }, { "id": 2, "name": "Spine Switch", "slug": "spine-switch", "color": "green" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_type.json0000644000175100017510000000106315215017101022157 0ustar00runnerrunner{ "id": 1, "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960", "part_number": "", "u_height": 16, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": false, "is_network_device": true, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/device_types.json0000644000175100017510000001116215215017101022343 0ustar00runnerrunner{ "count": 6, "next": null, "previous": null, "results": [ { "id": 2, "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214", "part_number": "", "u_height": 16, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": false, "is_network_device": true, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 }, { "id": 1, "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960", "part_number": "", "u_height": 16, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": false, "is_network_device": true, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 }, { "id": 3, "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q", "part_number": "", "u_height": 1, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": false, "is_network_device": true, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 }, { "id": 4, "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s", "part_number": "", "u_height": 1, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": false, "is_network_device": true, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 }, { "id": 5, "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148", "part_number": "", "u_height": 1, "is_full_depth": true, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": true, "is_pdu": false, "is_network_device": false, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 1 }, { "id": 6, "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9", "part_number": "", "u_height": 0, "is_full_depth": false, "interface_ordering": { "value": 1, "label": "Slot/position" }, "is_console_server": false, "is_pdu": true, "is_network_device": false, "subdevice_role": null, "comments": "", "custom_fields": {}, "instance_count": 2 } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/devices.json0000644000175100017510000005305515215017101021311 0ustar00runnerrunner{ "count": 11, "next": null, "previous": null, "results": [ { "id": 2, "name": "test1-core1", "display_name": "test1-core1", "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "role": { "id": 3, "url": "http://localhost:8000/api/dcim/device-roles/3/", "name": "Core Switch", "slug": "core-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 17, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": { "id": 5, "url": "http://localhost:8000/api/ipam/ip-addresses/5/", "family": 4, "address": "10.0.254.1/24" }, "primary_ip4": { "id": 5, "url": "http://localhost:8000/api/ipam/ip-addresses/5/", "family": 4, "address": "10.0.254.1/24" }, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 8, "name": "test1-core2", "display_name": "test1-core2", "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "role": { "id": 3, "url": "http://localhost:8000/api/dcim/device-roles/3/", "name": "Core Switch", "slug": "core-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "67856734534", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "position": 17, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": { "id": 19, "url": "http://localhost:8000/api/ipam/ip-addresses/19/", "family": 4, "address": "10.0.254.2/32" }, "primary_ip4": { "id": 19, "url": "http://localhost:8000/api/ipam/ip-addresses/19/", "family": 4, "address": "10.0.254.2/32" }, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 1, "name": "test1-edge1", "display_name": "test1-edge1", "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "role": { "id": 1, "url": "http://localhost:8000/api/dcim/device-roles/1/", "name": "Router", "slug": "router" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "5555555555", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 1, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": { "id": 1, "url": "http://localhost:8000/api/ipam/ip-addresses/1/", "family": 4, "address": "10.0.255.1/32" }, "primary_ip4": { "id": 1, "url": "http://localhost:8000/api/ipam/ip-addresses/1/", "family": 4, "address": "10.0.255.1/32" }, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 7, "name": "test1-edge2", "display_name": "test1-edge2", "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "role": { "id": 1, "url": "http://localhost:8000/api/dcim/device-roles/1/", "name": "Router", "slug": "router" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "7567356345", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "position": 1, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": { "id": 3, "url": "http://localhost:8000/api/ipam/ip-addresses/3/", "family": 4, "address": "10.0.255.2/32" }, "primary_ip4": { "id": 3, "url": "http://localhost:8000/api/ipam/ip-addresses/3/", "family": 4, "address": "10.0.255.2/32" }, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 4, "name": "test1-leaf1", "display_name": "test1-leaf1", "device_type": { "id": 4, "url": "http://localhost:8000/api/dcim/device-types/4/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s" }, "role": { "id": 4, "url": "http://localhost:8000/api/dcim/device-roles/4/", "name": "Leaf Switch", "slug": "leaf-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 34, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 5, "name": "test1-leaf2", "display_name": "test1-leaf2", "device_type": { "id": 4, "url": "http://localhost:8000/api/dcim/device-types/4/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s" }, "role": { "id": 4, "url": "http://localhost:8000/api/dcim/device-roles/4/", "name": "Leaf Switch", "slug": "leaf-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "9823478293748", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "position": 34, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 9, "name": "test1-oob1", "display_name": "test1-oob1", "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "role": { "id": 5, "url": "http://localhost:8000/api/dcim/device-roles/5/", "name": "OOB Switch", "slug": "oob-switch" }, "tenant": null, "platform": { "id": 2, "url": "http://localhost:8000/api/dcim/platforms/2/", "name": "Opengear", "slug": "opengear" }, "serial": "98273942938", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 42, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 11, "name": "test1-pdu1", "display_name": "test1-pdu1", "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "role": { "id": 6, "url": "http://localhost:8000/api/dcim/device-roles/6/", "name": "PDU", "slug": "pdu" }, "tenant": null, "platform": null, "serial": "", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": null, "face": null, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 12, "name": "test1-pdu2", "display_name": "test1-pdu2", "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "role": { "id": 6, "url": "http://localhost:8000/api/dcim/device-roles/6/", "name": "PDU", "slug": "pdu" }, "tenant": null, "platform": null, "serial": "", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "position": null, "face": null, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 3, "name": "test1-spine1", "display_name": "test1-spine1", "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "role": { "id": 2, "url": "http://localhost:8000/api/dcim/device-roles/2/", "name": "Spine Switch", "slug": "spine-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 1, "url": "http://localhost:8000/api/dcim/racks/1/", "name": "A1R1", "display_name": "A1R1 (T23A01)" }, "position": 33, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} }, { "id": 6, "name": "test1-spine2", "display_name": "test1-spine2", "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "role": { "id": 2, "url": "http://localhost:8000/api/dcim/device-roles/2/", "name": "Spine Switch", "slug": "spine-switch" }, "tenant": null, "platform": { "id": 1, "url": "http://localhost:8000/api/dcim/platforms/1/", "name": "Juniper Junos", "slug": "juniper-junos" }, "serial": "45649818158", "asset_tag": null, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "position": 33, "face": { "value": 0, "label": "Front" }, "parent_device": null, "status": { "value": true, "label": "Active" }, "primary_ip": null, "primary_ip4": null, "primary_ip6": null, "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface.json0000644000175100017510000000272415215017101021624 0ustar00runnerrunner{ "id": 1, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "enabled": true, "lag": { "id": 223, "url": "http://localhost:8000/api/dcim/interfaces/223/", "name": "ae0" }, "mtu": null, "mac_address": null, "mgmt_only": false, "description": "", "is_connected": true, "connected_endpoints": [ { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "display": "tst-endpoint", "name": "tst-endpoint" } ], "connected_endpoints_type": "dcim.device", "connected_endpoints_reachable": true, "cable": { "id": 1, "url": "http://localhost:8000/api/dcim/cables/1/", "label": "" }, "mode": { "value": "tagged", "label": "Tagged", "id": 200 }, "untagged_vlan": { "id": 2, "url": "http://localhost:8000/api/ipam/vlans/792/", "vid": 2069, "name": "v2069", "display_name": "2069 (v2069)" }, "tagged_vlans": [ { "id": 3, "url": "http://localhost:8000/api/ipam/vlans/248/", "vid": 1210, "name": "v1210", "display_name": "1210 (v1210)" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface_connection.json0000644000175100017510000000173715215017101024046 0ustar00runnerrunner{ "id": 1, "interface_a": { "id": 99, "url": "http://localhost:8000/api/dcim/interfaces/99/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "et-0/0/48", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 15, "url": "http://localhost:8000/api/dcim/interfaces/15/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/0/1", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface_connections.json0000644000175100017510000005276015215017101024233 0ustar00runnerrunner{ "count": 17, "next": null, "previous": null, "results": [ { "id": 22, "interface_a": { "id": 9, "url": "http://localhost:8000/api/dcim/interfaces/9/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 218, "url": "http://localhost:8000/api/dcim/interfaces/218/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 23, "interface_a": { "id": 8, "url": "http://localhost:8000/api/dcim/interfaces/8/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 206, "url": "http://localhost:8000/api/dcim/interfaces/206/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 24, "interface_a": { "id": 7, "url": "http://localhost:8000/api/dcim/interfaces/7/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 212, "url": "http://localhost:8000/api/dcim/interfaces/212/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 21, "interface_a": { "id": 194, "url": "http://localhost:8000/api/dcim/interfaces/194/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 200, "url": "http://localhost:8000/api/dcim/interfaces/200/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 17, "interface_a": { "id": 192, "url": "http://localhost:8000/api/dcim/interfaces/192/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 175, "url": "http://localhost:8000/api/dcim/interfaces/175/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 20, "interface_a": { "id": 191, "url": "http://localhost:8000/api/dcim/interfaces/191/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 197, "url": "http://localhost:8000/api/dcim/interfaces/197/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 16, "interface_a": { "id": 189, "url": "http://localhost:8000/api/dcim/interfaces/189/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 37, "url": "http://localhost:8000/api/dcim/interfaces/37/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 8, "interface_a": { "id": 92, "url": "http://localhost:8000/api/dcim/interfaces/92/", "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "xe-0/0/47", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 145, "url": "http://localhost:8000/api/dcim/interfaces/145/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "xe-0/0/47", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 7, "interface_a": { "id": 91, "url": "http://localhost:8000/api/dcim/interfaces/91/", "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "xe-0/0/46", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 144, "url": "http://localhost:8000/api/dcim/interfaces/144/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "xe-0/0/46", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 6, "interface_a": { "id": 47, "url": "http://localhost:8000/api/dcim/interfaces/47/", "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "et-0/0/49", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 152, "url": "http://localhost:8000/api/dcim/interfaces/152/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 5, "interface_a": { "id": 46, "url": "http://localhost:8000/api/dcim/interfaces/46/", "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "et-0/0/48", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 14, "url": "http://localhost:8000/api/dcim/interfaces/14/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/0/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 4, "interface_a": { "id": 100, "url": "http://localhost:8000/api/dcim/interfaces/100/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "et-0/0/49", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 153, "url": "http://localhost:8000/api/dcim/interfaces/153/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/1", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 3, "interface_a": { "id": 99, "url": "http://localhost:8000/api/dcim/interfaces/99/", "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "et-0/0/48", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 15, "url": "http://localhost:8000/api/dcim/interfaces/15/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/0/1", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 25, "interface_a": { "id": 217, "url": "http://localhost:8000/api/dcim/interfaces/217/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 205, "url": "http://localhost:8000/api/dcim/interfaces/205/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 26, "interface_a": { "id": 216, "url": "http://localhost:8000/api/dcim/interfaces/216/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 211, "url": "http://localhost:8000/api/dcim/interfaces/211/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 19, "interface_a": { "id": 198, "url": "http://localhost:8000/api/dcim/interfaces/198/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 179, "url": "http://localhost:8000/api/dcim/interfaces/179/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } }, { "id": 18, "interface_a": { "id": 195, "url": "http://localhost:8000/api/dcim/interfaces/195/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "interface_b": { "id": 41, "url": "http://localhost:8000/api/dcim/interfaces/41/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/2/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" }, "connection_status": { "value": true, "label": "Connected" } } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface_template.json0000644000175100017510000000076415215017101023521 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "mgmt_only": true }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface_templates.json0000644000175100017510000000262115215017101023676 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 4, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "mgmt_only": true }, { "id": 5, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "fxp0 (RE1)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "mgmt_only": true } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interface_trace.json0000644000175100017510000000677015215017101023007 0ustar00runnerrunner[ [ { "id": 39126, "url": "http://localhost:8000/api/dcim/interfaces/39126/", "device": { "id": 4747, "url": "http://localhost:8000/api/dcim/devices/4747/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "em1", "cable": 9911, "connection_status": { "value": false, "label": "Not Connected" } }, { "id": 9911, "url": "http://localhost:8000/api/dcim/cables/9911/", "type": "", "status": "planned", "label": "", "color": "", "length": null, "length_unit": "" }, { "id": 5583, "url": "http://localhost:8000/api/dcim/front-ports/5583/", "device": { "id": 4430, "url": "http://localhost:8000/api/dcim/devices/4430/", "name": "test1-patchpanel1", "display_name": "test1-patchpanel1" }, "name": "pair-11 (ports 21-22)", "cable": 9911 } ], [ { "id": 3736, "url": "http://localhost:8000/api/dcim/rear-ports/3736/", "device": { "id": 4430, "url": "http://localhost:8000/api/dcim/devices/4430/", "name": "test1-patchpanel1", "display_name": "test1-patchpanel1" }, "name": "port-2", "cable": 9229 }, { "id": 9229, "url": "http://localhost:8000/api/dcim/cables/9229/", "type": "mmf-om4", "status": "planned", "label": "", "color": "", "length": null, "length_unit": "" }, { "id": 3768, "url": "http://localhost:8000/api/dcim/rear-ports/3768/", "device": { "id": 4436, "url": "http://localhost:8000/api/dcim/devices/4436/", "name": "test1-patchpanel2", "display_name": "test1-patchpanel2" }, "name": "port-2", "cable": 9229 } ], [ { "id": 5655, "url": "http://localhost:8000/api/dcim/front-ports/5655/", "device": { "id": 4436, "url": "http://localhost:8000/api/dcim/devices/4436/", "name": "test1-patchpanel2", "display_name": "test1-patchpanel2" }, "name": "pair-11 (ports 21-22)", "cable": 9240 }, { "id": 9240, "url": "http://localhost:8000/api/dcim/cables/9240/", "type": "mmf-om4", "status": "planned", "label": "", "color": "", "length": null, "length_unit": "" }, { "id": 35473, "url": "http://localhost:8000/api/dcim/interfaces/35473/", "device": { "id": 3930, "url": "http://localhost:8000/api/dcim/devices/3930/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "Ethernet11", "cable": 9240, "connection_status": { "value": false, "label": "Not Connected" } } ] ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interfaces.json0000644000175100017510000003356715215017101022020 0ustar00runnerrunner{ "count": 221, "next": null, "previous": "http://localhost:8000/api/dcim/interfaces/?limit=50&offset=150", "results": [ { "id": 162, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/18", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 163, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/19", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 164, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 165, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/20", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 166, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/21", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 167, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/22", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 168, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 169, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/4", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 170, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/5", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 171, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/6", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 172, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/7", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 173, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/8", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 174, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/9", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 175, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 17, "url": "http://localhost:8000/api/dcim/interface-connections/17/", "connection_status": true }, "connected_interface": { "id": 192, "url": "http://localhost:8000/api/dcim/interfaces/192/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 176, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 177, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 178, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 179, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 19, "url": "http://localhost:8000/api/dcim/interface-connections/19/", "connection_status": true }, "connected_interface": { "id": 198, "url": "http://localhost:8000/api/dcim/interfaces/198/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 180, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 181, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 182, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interfaces_1.json0000644000175100017510000012450715215017101022233 0ustar00runnerrunner{ "count": 221, "next": "http://localhost:8000/api/dcim/interfaces/?limit=50&offset=50", "previous": null, "results": [ { "id": 189, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 16, "url": "http://localhost:8000/api/dcim/interface-connections/16/", "connection_status": true }, "connected_interface": { "id": 37, "url": "http://localhost:8000/api/dcim/interfaces/37/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 190, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 191, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 20, "url": "http://localhost:8000/api/dcim/interface-connections/20/", "connection_status": true }, "connected_interface": { "id": 197, "url": "http://localhost:8000/api/dcim/interfaces/197/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 192, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 17, "url": "http://localhost:8000/api/dcim/interface-connections/17/", "connection_status": true }, "connected_interface": { "id": 175, "url": "http://localhost:8000/api/dcim/interfaces/175/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 193, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 194, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 21, "url": "http://localhost:8000/api/dcim/interface-connections/21/", "connection_status": true }, "connected_interface": { "id": 200, "url": "http://localhost:8000/api/dcim/interfaces/200/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 10, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 11, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "fxp0 (RE1)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 12, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 201, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 202, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/1", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 203, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/2", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 204, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 205, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 25, "url": "http://localhost:8000/api/dcim/interface-connections/25/", "connection_status": true }, "connected_interface": { "id": 217, "url": "http://localhost:8000/api/dcim/interfaces/217/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 206, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 23, "url": "http://localhost:8000/api/dcim/interface-connections/23/", "connection_status": true }, "connected_interface": { "id": 8, "url": "http://localhost:8000/api/dcim/interfaces/8/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 195, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 18, "url": "http://localhost:8000/api/dcim/interface-connections/18/", "connection_status": true }, "connected_interface": { "id": 41, "url": "http://localhost:8000/api/dcim/interfaces/41/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/2/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 196, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 197, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/0/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 20, "url": "http://localhost:8000/api/dcim/interface-connections/20/", "connection_status": true }, "connected_interface": { "id": 191, "url": "http://localhost:8000/api/dcim/interfaces/191/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/0/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 198, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 19, "url": "http://localhost:8000/api/dcim/interface-connections/19/", "connection_status": true }, "connected_interface": { "id": 179, "url": "http://localhost:8000/api/dcim/interfaces/179/", "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 199, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 200, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 21, "url": "http://localhost:8000/api/dcim/interface-connections/21/", "connection_status": true }, "connected_interface": { "id": 194, "url": "http://localhost:8000/api/dcim/interfaces/194/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 186, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 187, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "fxp0 (RE1)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 188, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 207, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 208, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/1", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 209, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/2", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 210, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 211, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 26, "url": "http://localhost:8000/api/dcim/interface-connections/26/", "connection_status": true }, "connected_interface": { "id": 216, "url": "http://localhost:8000/api/dcim/interfaces/216/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 212, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 24, "url": "http://localhost:8000/api/dcim/interface-connections/24/", "connection_status": true }, "connected_interface": { "id": 7, "url": "http://localhost:8000/api/dcim/interfaces/7/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 1, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 2, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "fxp0 (RE1)", "form_factor": { "value": 800, "label": "100BASE-TX (10/100ME)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 3, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 4, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "TEST", "connection": null, "connected_interface": null }, { "id": 5, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/1", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 6, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/2", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 7, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 24, "url": "http://localhost:8000/api/dcim/interface-connections/24/", "connection_status": true }, "connected_interface": { "id": 212, "url": "http://localhost:8000/api/dcim/interfaces/212/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 8, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 23, "url": "http://localhost:8000/api/dcim/interface-connections/23/", "connection_status": true }, "connected_interface": { "id": 206, "url": "http://localhost:8000/api/dcim/interfaces/206/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 9, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 22, "url": "http://localhost:8000/api/dcim/interface-connections/22/", "connection_status": true }, "connected_interface": { "id": 218, "url": "http://localhost:8000/api/dcim/interfaces/218/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 183, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "fxp0 (RE0)", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 184, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "fxp0 (RE1)", "form_factor": { "value": 800, "label": "100BASE-TX (10/100ME)" }, "lag": null, "mac_address": null, "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 185, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 213, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 214, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/1", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 215, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/2", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 216, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 26, "url": "http://localhost:8000/api/dcim/interface-connections/26/", "connection_status": true }, "connected_interface": { "id": 211, "url": "http://localhost:8000/api/dcim/interfaces/211/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 217, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 25, "url": "http://localhost:8000/api/dcim/interface-connections/25/", "connection_status": true }, "connected_interface": { "id": 205, "url": "http://localhost:8000/api/dcim/interfaces/205/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 218, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 22, "url": "http://localhost:8000/api/dcim/interface-connections/22/", "connection_status": true }, "connected_interface": { "id": 9, "url": "http://localhost:8000/api/dcim/interfaces/9/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 45, "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "em0", "form_factor": { "value": 1000, "label": "1000BASE-T (1GE)" }, "lag": null, "mac_address": "FF:EE:DD:33:22:11", "mgmt_only": true, "description": "", "connection": null, "connected_interface": null }, { "id": 46, "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "et-0/0/48", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 5, "url": "http://localhost:8000/api/dcim/interface-connections/5/", "connection_status": true }, "connected_interface": { "id": 14, "url": "http://localhost:8000/api/dcim/interfaces/14/", "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "et-0/0/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/interfaces_2.json0000644000175100017510000003356715215017101022241 0ustar00runnerrunner{ "count": 221, "next": null, "previous": "http://localhost:8000/api/dcim/interfaces/?limit=50&offset=150", "results": [ { "id": 162, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/18", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 163, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/19", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 164, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 165, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/20", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 166, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/21", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 167, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/22", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 168, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 169, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/4", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 170, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/5", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 171, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/6", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 172, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/7", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 173, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/8", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 174, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/0/9", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 175, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 17, "url": "http://localhost:8000/api/dcim/interface-connections/17/", "connection_status": true }, "connected_interface": { "id": 192, "url": "http://localhost:8000/api/dcim/interfaces/192/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 176, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 177, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 178, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/1/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 179, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/0", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 19, "url": "http://localhost:8000/api/dcim/interface-connections/19/", "connection_status": true }, "connected_interface": { "id": 198, "url": "http://localhost:8000/api/dcim/interfaces/198/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/0", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, { "id": 180, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/1", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 181, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, { "id": 182, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "et-0/2/3", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/inventory_item.json0000644000175100017510000000003615215017101022731 0ustar00runnerrunner{ "detail": "Not found." }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/inventory_items.json0000644000175100017510000000011515215017101023112 0ustar00runnerrunner{ "count": 0, "next": null, "previous": null, "results": [] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/manufacturer.json0000644000175100017510000000007515215017101022355 0ustar00runnerrunner{ "id": 1, "name": "Juniper", "slug": "juniper" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/manufacturers.json0000644000175100017510000000061615215017101022541 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "name": "Juniper", "slug": "juniper" }, { "id": 2, "name": "Opengear", "slug": "opengear" }, { "id": 3, "name": "ServerTech", "slug": "servertech" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/napalm.json0000644000175100017510000000044615215017101021133 0ustar00runnerrunner{ "get_facts": { "interface_list": [ "xe-0/0/0" ], "serial_number": "ABC!@#", "vendor": "PacketPusher", "os_version": "1.1", "hostname": "test-1", "model": "UnobtaniumX", "fqdn": "None", "uptime": 2 } }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/platform.json0000644000175100017510000000015415215017101021503 0ustar00runnerrunner{ "id": 1, "name": "Juniper Junos", "slug": "juniper-junos", "rpc_client": "juniper-junos" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/platforms.json0000644000175100017510000000057615215017101021676 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "name": "Juniper Junos", "slug": "juniper-junos", "rpc_client": "juniper-junos" }, { "id": 2, "name": "Opengear", "slug": "opengear", "rpc_client": "opengear" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_outlet.json0000644000175100017510000000034415215017101022410 0ustar00runnerrunner{ "id": 1, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA1", "connected_port": 1 }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_outlet_template.json0000644000175100017510000000062715215017101024307 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA1" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_outlet_templates.json0000644000175100017510000003127415215017101024474 0ustar00runnerrunner{ "count": 24, "next": null, "previous": null, "results": [ { "id": 4, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA1" }, { "id": 5, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA2" }, { "id": 6, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA3" }, { "id": 7, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA4" }, { "id": 8, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA5" }, { "id": 9, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA6" }, { "id": 10, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA7" }, { "id": 11, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AA8" }, { "id": 12, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB1" }, { "id": 13, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB2" }, { "id": 14, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB3" }, { "id": 15, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB4" }, { "id": 16, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB5" }, { "id": 17, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB6" }, { "id": 18, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB7" }, { "id": 19, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AB8" }, { "id": 20, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC1" }, { "id": 21, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC2" }, { "id": 22, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC3" }, { "id": 23, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC4" }, { "id": 24, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC5" }, { "id": 25, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC6" }, { "id": 26, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC7" }, { "id": 27, "device_type": { "id": 6, "url": "http://localhost:8000/api/dcim/device-types/6/", "manufacturer": { "id": 3, "url": "http://localhost:8000/api/dcim/manufacturers/3/", "name": "ServerTech", "slug": "servertech" }, "model": "CWG-24VYM415C9", "slug": "cwg-24vym415c9" }, "name": "AC8" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_outlets.json0000644000175100017510000003621515215017101022601 0ustar00runnerrunner{ "count": 48, "next": null, "previous": null, "results": [ { "id": 25, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA1", "connected_port": 1 }, { "id": 26, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA2", "connected_port": 5 }, { "id": 27, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA3", "connected_port": 13 }, { "id": 28, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA4", "connected_port": 9 }, { "id": 29, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA5", "connected_port": 16 }, { "id": 30, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA6", "connected_port": 20 }, { "id": 31, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA7", "connected_port": 24 }, { "id": 32, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA8", "connected_port": 12 }, { "id": 33, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB1", "connected_port": null }, { "id": 34, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB2", "connected_port": null }, { "id": 35, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB3", "connected_port": null }, { "id": 36, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB4", "connected_port": null }, { "id": 37, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB5", "connected_port": null }, { "id": 38, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB6", "connected_port": null }, { "id": 39, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB7", "connected_port": null }, { "id": 40, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AB8", "connected_port": null }, { "id": 41, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC1", "connected_port": null }, { "id": 42, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC2", "connected_port": null }, { "id": 43, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC3", "connected_port": null }, { "id": 44, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC4", "connected_port": null }, { "id": 45, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC5", "connected_port": null }, { "id": 46, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC6", "connected_port": null }, { "id": 47, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC7", "connected_port": null }, { "id": 48, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AC8", "connected_port": null }, { "id": 49, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA1", "connected_port": 2 }, { "id": 50, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA2", "connected_port": 6 }, { "id": 51, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA3", "connected_port": 14 }, { "id": 52, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA4", "connected_port": 10 }, { "id": 53, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA5", "connected_port": 15 }, { "id": 54, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA6", "connected_port": 19 }, { "id": 55, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA7", "connected_port": 23 }, { "id": 56, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA8", "connected_port": 11 }, { "id": 57, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB1", "connected_port": null }, { "id": 58, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB2", "connected_port": null }, { "id": 59, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB3", "connected_port": null }, { "id": 60, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB4", "connected_port": null }, { "id": 61, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB5", "connected_port": null }, { "id": 62, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB6", "connected_port": null }, { "id": 63, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB7", "connected_port": null }, { "id": 64, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AB8", "connected_port": null }, { "id": 65, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC1", "connected_port": null }, { "id": 66, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC2", "connected_port": null }, { "id": 67, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC3", "connected_port": null }, { "id": 68, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC4", "connected_port": null }, { "id": 69, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC5", "connected_port": null }, { "id": 70, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC6", "connected_port": null }, { "id": 71, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC7", "connected_port": null }, { "id": 72, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AC8", "connected_port": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_port.json0000644000175100017510000000101615215017101022055 0ustar00runnerrunner{ "id": 1, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "PEM0", "power_outlet": { "id": 25, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA1", "connected_port": 1 }, "connection_status": true }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_port_template.json0000644000175100017510000000060015215017101023746 0ustar00runnerrunner{ "id": 1, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "PEM0" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_port_templates.json0000644000175100017510000001525315215017101024143 0ustar00runnerrunner{ "count": 13, "next": null, "previous": null, "results": [ { "id": 5, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "PEM0" }, { "id": 6, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "PEM1" }, { "id": 7, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "PEM2" }, { "id": 8, "device_type": { "id": 2, "url": "http://localhost:8000/api/dcim/device-types/2/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "EX9214", "slug": "ex9214" }, "name": "PEM3" }, { "id": 1, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "PEM0" }, { "id": 2, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "PEM1" }, { "id": 3, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "PEM2" }, { "id": 4, "device_type": { "id": 1, "url": "http://localhost:8000/api/dcim/device-types/1/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "MX960", "slug": "mx960" }, "name": "PEM3" }, { "id": 11, "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "name": "PSU0" }, { "id": 12, "device_type": { "id": 3, "url": "http://localhost:8000/api/dcim/device-types/3/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-24Q", "slug": "qfx5100-24q" }, "name": "PSU1" }, { "id": 9, "device_type": { "id": 4, "url": "http://localhost:8000/api/dcim/device-types/4/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s" }, "name": "PSU0" }, { "id": 13, "device_type": { "id": 4, "url": "http://localhost:8000/api/dcim/device-types/4/", "manufacturer": { "id": 1, "url": "http://localhost:8000/api/dcim/manufacturers/1/", "name": "Juniper", "slug": "juniper" }, "model": "QFX5100-48S", "slug": "qfx5100-48s" }, "name": "PSU1" }, { "id": 14, "device_type": { "id": 5, "url": "http://localhost:8000/api/dcim/device-types/5/", "manufacturer": { "id": 2, "url": "http://localhost:8000/api/dcim/manufacturers/2/", "name": "Opengear", "slug": "opengear" }, "model": "CM4148", "slug": "cm4148" }, "name": "PSU" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/power_ports.json0000644000175100017510000003442115215017101022246 0ustar00runnerrunner{ "count": 25, "next": null, "previous": null, "results": [ { "id": 5, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "PEM0", "power_outlet": { "id": 26, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA2", "connected_port": 5 }, "connection_status": true }, { "id": 6, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "PEM1", "power_outlet": { "id": 50, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA2", "connected_port": 6 }, "connection_status": true }, { "id": 7, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "PEM2", "power_outlet": null, "connection_status": true }, { "id": 8, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "PEM3", "power_outlet": null, "connection_status": true }, { "id": 19, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "PEM0", "power_outlet": { "id": 54, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA6", "connected_port": 19 }, "connection_status": true }, { "id": 20, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "PEM1", "power_outlet": { "id": 30, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA6", "connected_port": 20 }, "connection_status": true }, { "id": 21, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "PEM2", "power_outlet": null, "connection_status": true }, { "id": 22, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "PEM3", "power_outlet": null, "connection_status": true }, { "id": 1, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "PEM0", "power_outlet": { "id": 25, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA1", "connected_port": 1 }, "connection_status": true }, { "id": 2, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "PEM1", "power_outlet": { "id": 49, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA1", "connected_port": 2 }, "connection_status": true }, { "id": 3, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "PEM2", "power_outlet": null, "connection_status": true }, { "id": 4, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "PEM3", "power_outlet": null, "connection_status": true }, { "id": 15, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "PEM0", "power_outlet": { "id": 53, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA5", "connected_port": 15 }, "connection_status": true }, { "id": 16, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "PEM1", "power_outlet": { "id": 29, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA5", "connected_port": 16 }, "connection_status": true }, { "id": 17, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "PEM2", "power_outlet": null, "connection_status": true }, { "id": 18, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "PEM3", "power_outlet": null, "connection_status": true }, { "id": 9, "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "PSU0", "power_outlet": { "id": 28, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA4", "connected_port": 9 }, "connection_status": true }, { "id": 10, "device": { "id": 4, "url": "http://localhost:8000/api/dcim/devices/4/", "name": "test1-leaf1", "display_name": "test1-leaf1" }, "name": "PSU1", "power_outlet": { "id": 52, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA4", "connected_port": 10 }, "connection_status": true }, { "id": 11, "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "PSU0", "power_outlet": { "id": 56, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA8", "connected_port": 11 }, "connection_status": true }, { "id": 12, "device": { "id": 5, "url": "http://localhost:8000/api/dcim/devices/5/", "name": "test1-leaf2", "display_name": "test1-leaf2" }, "name": "PSU1", "power_outlet": { "id": 32, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA8", "connected_port": 12 }, "connection_status": true }, { "id": 25, "device": { "id": 9, "url": "http://localhost:8000/api/dcim/devices/9/", "name": "test1-oob1", "display_name": "test1-oob1" }, "name": "PSU", "power_outlet": null, "connection_status": true }, { "id": 13, "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "PSU0", "power_outlet": { "id": 27, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA3", "connected_port": 13 }, "connection_status": true }, { "id": 14, "device": { "id": 3, "url": "http://localhost:8000/api/dcim/devices/3/", "name": "test1-spine1", "display_name": "test1-spine1" }, "name": "PSU1", "power_outlet": { "id": 51, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA3", "connected_port": 14 }, "connection_status": true }, { "id": 23, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "PSU0", "power_outlet": { "id": 55, "device": { "id": 12, "url": "http://localhost:8000/api/dcim/devices/12/", "name": "test1-pdu2", "display_name": "test1-pdu2" }, "name": "AA7", "connected_port": 23 }, "connection_status": true }, { "id": 24, "device": { "id": 6, "url": "http://localhost:8000/api/dcim/devices/6/", "name": "test1-spine2", "display_name": "test1-spine2" }, "name": "PSU1", "power_outlet": { "id": 31, "device": { "id": 11, "url": "http://localhost:8000/api/dcim/devices/11/", "name": "test1-pdu1", "display_name": "test1-pdu1" }, "name": "AA7", "connected_port": 24 }, "connection_status": true } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack.json0000644000175100017510000000115315215017101020577 0ustar00runnerrunner{ "id": 1, "name": "A1R1", "facility_id": "T23A01", "display_name": "A1R1 (T23A01)", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "tenant": null, "role": { "id": 1, "url": "http://localhost:8000/api/dcim/rack-roles/1/", "name": "Compute", "slug": "compute" }, "type": null, "width": { "value": 19, "label": "19 inches" }, "u_height": 42, "desc_units": false, "comments": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_group.json0000644000175100017510000000031015215017101022005 0ustar00runnerrunner{ "id": 1, "name": "TEST", "slug": "test", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" } }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_groups.json0000644000175100017510000000056315215017101022202 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST", "slug": "test", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" } } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_reservation.json0000644000175100017510000000046415215017101023224 0ustar00runnerrunner{ "id": 1, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "units": [ 42 ], "created": "2017-03-22T04:10:47.307156Z", "user": 1, "description": "Test Reservation" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_reservations.json0000644000175100017510000000077715215017101023416 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "rack": { "id": 2, "url": "http://localhost:8000/api/dcim/racks/2/", "name": "A1R2", "display_name": "A1R2 (T24A01)" }, "units": [ 42 ], "created": "2017-03-22T04:10:47.307156Z", "user": 1, "description": "Test Reservation" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_role.json0000644000175100017510000000011615215017101021616 0ustar00runnerrunner{ "id": 1, "name": "Test", "slug": "test", "color": "aa1409" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_roles.json0000644000175100017510000000032115215017101021777 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "Test", "slug": "test", "color": "aa1409" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/rack_u.json0000644000175100017510000000146515215017101021131 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 48, "name": "U1", "face": 0, "device": { "id": 130, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "tst-device1", "display_name": "tst-device1" } }, { "id": 47, "name": "U2", "face": 0, "device": null }, { "id": 46, "name": "U3", "face": 0, "device": { "id": 1859, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "tst-device2", "display_name": "tst-device2" } } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/racks.json0000644000175100017510000000263315215017101020766 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "name": "A1R1", "facility_id": "T23A01", "display_name": "A1R1 (T23A01)", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "tenant": null, "role": null, "type": null, "width": { "value": 19, "label": "19 inches" }, "u_height": 42, "desc_units": false, "comments": "", "custom_fields": {} }, { "id": 2, "name": "A1R2", "facility_id": "T24A01", "display_name": "A1R2 (T24A01)", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "tenant": null, "role": null, "type": null, "width": { "value": 19, "label": "19 inches" }, "u_height": 42, "desc_units": false, "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/region.json0000644000175100017510000000011315215017101021135 0ustar00runnerrunner{ "id": 1, "name": "TEST", "slug": "test", "parent": null }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/regions.json0000644000175100017510000000031615215017101021325 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST", "slug": "test", "parent": null } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/site.json0000644000175100017510000000113215215017101020620 0ustar00runnerrunner{ "id": 1, "name": "TEST1", "slug": "test1", "region": null, "tenant": null, "facility": "Test Facility", "asn": 65535, "physical_address": "555 Test Ave.\r\nTest, NY 55555", "shipping_address": "", "contact_name": "", "contact_phone": "", "contact_email": "", "comments": "", "custom_fields": { "test_custom": "Hello", "test_selection": { "value": 2, "label": "second" } }, "count_prefixes": 2, "count_vlans": 1, "count_racks": 2, "count_devices": 11, "count_circuits": 0 }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/sites.json0000644000175100017510000000141015215017101021002 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST1", "slug": "test1", "region": null, "tenant": null, "facility": "Test Facility", "asn": 65535, "physical_address": "555 Test Ave.\r\nTest, NY 55555", "shipping_address": "", "contact_name": "", "contact_phone": "", "contact_email": "", "comments": "", "custom_fields": { "test_custom": "Hello" }, "count_prefixes": 2, "count_vlans": 1, "count_racks": 2, "count_devices": 11, "count_circuits": 0 } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/virtual_chassis_device.json0000644000175100017510000000036615215017101024406 0ustar00runnerrunner{ "id": 1, "master": { "id": 5654, "url": "http://localhost:8000/api/dcim/devices/5654/", "name": "01-0001-e214-as01 (SW1)", "display_name": "01-0001-e214-as01 (SW1)" }, "domain": "test-domain-1" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/dcim/virtual_chassis_devices.json0000644000175100017510000000136315215017101024567 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "master": { "id": 5654, "url": "http://localhost:8000/api/dcim/devices/5654/", "name": "01-0001-e214-as01 (SW1)", "display_name": "01-0001-e214-as01 (SW1)" }, "domain": "test-domain-1" }, { "id": 2, "master": { "id": 5635, "url": "http://localhost:8000/netbox/api/dcim/devices/5635/", "name": "hercules.router.com (VSS-SW1)", "display_name": "hercules.router.com (VSS-SW1)" }, "domain": "test-domain-2" } ] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9625514 pynetbox-7.8.0/tests/fixtures/ipam/0000755000175100017510000000000015215017112017000 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/aggregate.json0000644000175100017510000000042715215017101021622 0ustar00runnerrunner{ "id": 1, "family": 4, "prefix": "10.0.0.0/8", "rir": { "id": 1, "url": "http://localhost:8000/api/ipam/rirs/1/", "name": "RFC1918", "slug": "rfc1918" }, "date_added": null, "description": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/aggregates.json0000644000175100017510000000073215215017101022004 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "family": 4, "prefix": "10.0.0.0/8", "rir": { "id": 1, "url": "http://localhost:8000/api/ipam/rirs/1/", "name": "RFC1918", "slug": "rfc1918" }, "date_added": null, "description": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/available-ips-post.json0000644000175100017510000000027315215017101023367 0ustar00runnerrunner{ "id": 1, "address": "10.1.1.1/32", "vrf": null, "tenant": null, "status": 1, "role": null, "interface": null, "description": "", "nat_inside": null }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/available-ips.json0000644000175100017510000000041215215017101022377 0ustar00runnerrunner[ { "family": 4, "address": "10.1.1.2/27", "vrf": null }, { "family": 4, "address": "10.1.1.3/27", "vrf": null }, { "family": 4, "address": "10.1.1.7/27", "vrf": null } ]././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/available-prefixes-post.json0000644000175100017510000000037115215017101024420 0ustar00runnerrunner[ { "id": 4, "prefix": "10.1.1.0/30", "site": null, "vrf": null, "tenant": null, "vlan": null, "status": 1, "role": null, "is_pool": false, "description": "" } ]././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/available-prefixes.json0000644000175100017510000000013115215017101023427 0ustar00runnerrunner[ { "family": 4, "prefix": "10.1.1.0/24", "vrf": null } ]././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/ip_address.json0000644000175100017510000000142215215017101022005 0ustar00runnerrunner{ "id": 1, "family": 4, "address": "10.0.255.1/32", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 3, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/ip_addresses.json0000644000175100017510000007003615215017101022344 0ustar00runnerrunner{ "count": 18, "next": null, "previous": null, "results": [ { "id": 5, "family": 4, "address": "10.0.254.1/24", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 12, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 19, "family": 4, "address": "10.0.254.2/32", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 188, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 1, "family": 4, "address": "10.0.255.1/32", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 3, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 3, "family": 4, "address": "10.0.255.2/32", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 185, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "lo0", "form_factor": { "value": 0, "label": "Virtual" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 11, "family": 4, "address": "10.15.20.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 7, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 24, "url": "http://localhost:8000/api/dcim/interface-connections/24/", "connection_status": true }, "connected_interface": { "id": 212, "url": "http://localhost:8000/api/dcim/interfaces/212/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 8, "family": 4, "address": "10.15.21.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 218, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 22, "url": "http://localhost:8000/api/dcim/interface-connections/22/", "connection_status": true }, "connected_interface": { "id": 9, "url": "http://localhost:8000/api/dcim/interfaces/9/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 9, "family": 4, "address": "10.15.21.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 9, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 22, "url": "http://localhost:8000/api/dcim/interface-connections/22/", "connection_status": true }, "connected_interface": { "id": 218, "url": "http://localhost:8000/api/dcim/interfaces/218/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 10, "family": 4, "address": "10.15.22.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 8, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 23, "url": "http://localhost:8000/api/dcim/interface-connections/23/", "connection_status": true }, "connected_interface": { "id": 206, "url": "http://localhost:8000/api/dcim/interfaces/206/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/5", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 13, "family": 4, "address": "10.15.22.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 206, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 23, "url": "http://localhost:8000/api/dcim/interface-connections/23/", "connection_status": true }, "connected_interface": { "id": 8, "url": "http://localhost:8000/api/dcim/interfaces/8/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 17, "family": 4, "address": "10.15.22.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 212, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/5", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 24, "url": "http://localhost:8000/api/dcim/interface-connections/24/", "connection_status": true }, "connected_interface": { "id": 7, "url": "http://localhost:8000/api/dcim/interfaces/7/", "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 12, "family": 4, "address": "10.16.20.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 216, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/3", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 26, "url": "http://localhost:8000/api/dcim/interface-connections/26/", "connection_status": true }, "connected_interface": { "id": 211, "url": "http://localhost:8000/api/dcim/interfaces/211/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 16, "family": 4, "address": "10.16.20.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 211, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 26, "url": "http://localhost:8000/api/dcim/interface-connections/26/", "connection_status": true }, "connected_interface": { "id": 216, "url": "http://localhost:8000/api/dcim/interfaces/216/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/3", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 14, "family": 4, "address": "10.16.22.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 217, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 25, "url": "http://localhost:8000/api/dcim/interface-connections/25/", "connection_status": true }, "connected_interface": { "id": 205, "url": "http://localhost:8000/api/dcim/interfaces/205/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 15, "family": 4, "address": "10.16.22.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 205, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "xe-0/0/4", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 25, "url": "http://localhost:8000/api/dcim/interface-connections/25/", "connection_status": true }, "connected_interface": { "id": 217, "url": "http://localhost:8000/api/dcim/interfaces/217/", "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/4", "form_factor": 1200, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 4, "family": 4, "address": "169.254.1.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 213, "device": { "id": 7, "url": "http://localhost:8000/api/dcim/devices/7/", "name": "test1-edge2", "display_name": "test1-edge2" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 20, "family": 4, "address": "169.254.1.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 200, "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 21, "url": "http://localhost:8000/api/dcim/interface-connections/21/", "connection_status": true }, "connected_interface": { "id": 194, "url": "http://localhost:8000/api/dcim/interfaces/194/", "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 21, "family": 4, "address": "169.254.1.2/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 194, "device": { "id": 2, "url": "http://localhost:8000/api/dcim/devices/2/", "name": "test1-core1", "display_name": "test1-core1" }, "name": "et-0/1/2", "form_factor": { "value": 1400, "label": "QSFP+ (40GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "", "connection": { "id": 21, "url": "http://localhost:8000/api/dcim/interface-connections/21/", "connection_status": true }, "connected_interface": { "id": 200, "url": "http://localhost:8000/api/dcim/interfaces/200/", "device": { "id": 8, "url": "http://localhost:8000/api/dcim/devices/8/", "name": "test1-core2", "display_name": "test1-core2" }, "name": "et-0/1/2", "form_factor": 1400, "mac_address": null, "mgmt_only": false, "description": "" } }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} }, { "id": 2, "family": 4, "address": "169.254.254.1/31", "vrf": null, "tenant": null, "status": { "value": 1, "label": "Active" }, "interface": { "id": 4, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test1-edge1", "display_name": "test1-edge1" }, "name": "xe-0/0/0", "form_factor": { "value": 1200, "label": "SFP+ (10GE)" }, "lag": null, "mac_address": null, "mgmt_only": false, "description": "TEST", "connection": null, "connected_interface": null }, "description": "", "nat_inside": null, "nat_outside": null, "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/prefix.json0000644000175100017510000000105515215017101021167 0ustar00runnerrunner{ "id": 1, "family": 4, "prefix": "10.1.1.0/24", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "vrf": null, "tenant": null, "vlan": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "is_pool": false, "description": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/prefixes.json0000644000175100017510000000314115215017101021515 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 2, "family": 4, "prefix": "10.0.255.0/24", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "vrf": null, "tenant": null, "vlan": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "is_pool": false, "description": "", "custom_fields": {} }, { "id": 1, "family": 4, "prefix": "10.1.1.0/24", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "vrf": null, "tenant": null, "vlan": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "is_pool": false, "description": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/rir.json0000644000175100017510000000012615215017101020464 0ustar00runnerrunner{ "id": 1, "name": "RFC1918", "slug": "rfc1918", "is_private": false }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/rirs.json0000644000175100017510000000033115215017101020645 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "RFC1918", "slug": "rfc1918", "is_private": false } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/role.json0000644000175100017510000000013115215017101020625 0ustar00runnerrunner{ "id": 1, "name": "Lab Network", "slug": "lab-network", "weight": 1000 }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/roles.json0000644000175100017510000000033415215017101021015 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "Lab Network", "slug": "lab-network", "weight": 1000 } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vlan.json0000644000175100017510000000104315215017101020627 0ustar00runnerrunner{ "id": 3, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "vid": 1210, "name": "v1210", "tenant": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "description": "", "display_name": "1210 (v1210)", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vlan_group.json0000644000175100017510000000031015215017101022037 0ustar00runnerrunner{ "id": 1, "name": "TEST", "slug": "test", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" } }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vlan_groups.json0000644000175100017510000000056315215017101022234 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST", "slug": "test", "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" } } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vlans.json0000644000175100017510000000445415215017101021023 0ustar00runnerrunner{ "count": 3, "next": null, "previous": null, "results": [ { "id": 1, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "vid": 999, "name": "TEST", "tenant": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "description": "", "display_name": "999 (TEST)", "custom_fields": {} }, { "id": 2, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "vid": 2069, "name": "v2069", "tenant": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "description": "", "display_name": "2069 (v2069)", "custom_fields": {} }, { "id": 3, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "group": null, "vid": 1210, "name": "v1210", "tenant": null, "status": { "value": 1, "label": "Active" }, "role": { "id": 1, "url": "http://localhost:8000/api/ipam/roles/1/", "name": "Lab Network", "slug": "lab-network" }, "description": "", "display_name": "1210 (v1210)", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vrf.json0000644000175100017510000000023015215017101020461 0ustar00runnerrunner{ "id": 1, "name": "TEST", "rd": "65535:1", "tenant": null, "enforce_unique": true, "description": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/ipam/vrfs.json0000644000175100017510000000046315215017101020654 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST", "rd": "65535:1", "tenant": null, "enforce_unique": true, "description": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9631054 pynetbox-7.8.0/tests/fixtures/tenancy/0000755000175100017510000000000015215017112017513 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/tenancy/tenant.json0000644000175100017510000000046415215017101021701 0ustar00runnerrunner{ "id": 1, "name": "TEST Tenant 1", "slug": "test-tenant-1", "group": { "id": 1, "url": "http://localhost:8000/api/tenancy/tenant-groups/1/", "name": "TEST Group", "slug": "test-group" }, "description": "", "comments": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/tenancy/tenant_group.json0000644000175100017510000000010315215017101023103 0ustar00runnerrunner{ "id": 1, "name": "TEST Group", "slug": "test-group" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/tenancy/tenant_groups.json0000644000175100017510000000027615215017101023301 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST Group", "slug": "test-group" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/tenancy/tenants.json0000644000175100017510000000163515215017101022065 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "name": "TEST Tenant 1", "slug": "test-tenant-1", "group": { "id": 1, "url": "http://localhost:8000/api/tenancy/tenant-groups/1/", "name": "TEST Group", "slug": "test-group" }, "description": "", "comments": "", "custom_fields": {} }, { "id": 2, "name": "TEST Tenant 2", "slug": "test-tenant-2", "group": { "id": 1, "url": "http://localhost:8000/api/tenancy/tenant-groups/1/", "name": "TEST Group", "slug": "test-group" }, "description": "", "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1781800521.964047 pynetbox-7.8.0/tests/fixtures/users/0000755000175100017510000000000015215017112017213 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/group.json0000644000175100017510000000005215215017101021235 0ustar00runnerrunner{ "id": 1, "name": "usergroup1" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/groups.json0000644000175100017510000000035015215017101021421 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "name": "usergroup1" }, { "id": 2, "name": "usergroup2" } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/permission.json0000644000175100017510000000037615215017101022302 0ustar00runnerrunner{ "id": 1, "name": "permission1", "users": [ { "username": "user1" } ], "constraints": [ { "status": "active" }, { "region__name": "Europe" } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/permissions.json0000644000175100017510000000035215215017101022457 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "name": "permission1" }, { "id": 2, "name": "permission2" } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/unknown_model.json0000644000175100017510000000006115215017101022760 0ustar00runnerrunner{ "id": 1, "display": "Unknown object" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/user.json0000644000175100017510000000005115215017101021056 0ustar00runnerrunner{ "id": 1, "username": "user1" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/users/users.json0000644000175100017510000000034615215017101021250 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "username": "user1" }, { "id": 2, "username": "user2" } ] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9656794 pynetbox-7.8.0/tests/fixtures/virtualization/0000755000175100017510000000000015215017112021136 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/cluster.json0000644000175100017510000000112215215017101023504 0ustar00runnerrunner{ "id": 1, "name": "vm-test-cluster", "type": { "id": 1, "url": "http://localhost:8000/api/virtualization/cluster-types/1/", "name": "vm-test-type", "slug": "vm-test-type" }, "group": { "id": 1, "url": "http://localhost:8000/api/virtualization/cluster-groups/1/", "name": "vm-test-group", "slug": "vm-test-group" }, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "comments": "", "custom_fields": {} }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/cluster_group.json0000644000175100017510000000011115215017101024715 0ustar00runnerrunner{ "id": 1, "name": "vm-test-group", "slug": "vm-test-group" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/cluster_groups.json0000644000175100017510000000030415215017101025104 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "vm-test-group", "slug": "vm-test-group" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/cluster_type.json0000644000175100017510000000010715215017101024547 0ustar00runnerrunner{ "id": 1, "name": "vm-test-type", "slug": "vm-test-type" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/cluster_types.json0000644000175100017510000000030215215017101024727 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "vm-test-type", "slug": "vm-test-type" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/clusters.json0000644000175100017510000000154515215017101023700 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "vm-test-cluster", "type": { "id": 1, "url": "http://localhost:8000/api/virtualization/cluster-types/1/", "name": "vm-test-type", "slug": "vm-test-type" }, "group": { "id": 1, "url": "http://localhost:8000/api/virtualization/cluster-groups/1/", "name": "vm-test-group", "slug": "vm-test-group" }, "site": { "id": 1, "url": "http://localhost:8000/api/dcim/sites/1/", "name": "TEST1", "slug": "test1" }, "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/interface.json0000644000175100017510000000042715215017101023772 0ustar00runnerrunner{ "id": 223, "name": "eth0", "virtual_machine": { "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", "name": "vm-test01" }, "enabled": true, "mac_address": null, "mtu": null, "description": "" }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/interfaces.json0000644000175100017510000000152315215017101024153 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 223, "name": "eth0", "virtual_machine": { "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", "name": "vm-test01" }, "enabled": true, "mac_address": null, "mtu": null, "description": "" }, { "id": 224, "name": "eth1", "virtual_machine": { "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machines/1/", "name": "vm-test01" }, "enabled": true, "mac_address": null, "mtu": null, "description": "" } ] }././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/virtual_machine.json0000644000175100017510000000122615215017101025202 0ustar00runnerrunner{ "id": 1, "name": "vm-test01", "status": { "value": 1, "label": "Active" }, "cluster": null, "device": { "id": 1, "url": "http://localhost:8000/api/dcim/devices/1/", "name": "test-device" }, "virtual_machine_type": { "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machine-types/1/", "name": "Standard", "slug": "standard" }, "role": null, "tenant": null, "platform": null, "primary_ip4": null, "primary_ip6": null, "vcpus": 2, "memory": 1024, "disk": 500, "comments": "", "custom_fields": {} } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/virtual_machine_type.json0000644000175100017510000000022015215017101026234 0ustar00runnerrunner{ "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machine-types/1/", "name": "Standard", "slug": "standard" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/virtual_machine_types.json0000644000175100017510000000042315215017101026424 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "url": "http://localhost:8000/api/virtualization/virtual-machine-types/1/", "name": "Standard", "slug": "standard" } ] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/virtualization/virtual_machines.json0000644000175100017510000000133215215017101025363 0ustar00runnerrunner{ "count": 1, "next": null, "previous": null, "results": [ { "id": 1, "name": "vm-test01", "status": { "value": 1, "label": "Active" }, "cluster": { "id": 1, "url": "http://localhost:8000/api/virtualization/clusters/1/", "name": "vm-test-cluster" }, "role": null, "tenant": null, "platform": null, "primary_ip4": null, "primary_ip6": null, "vcpus": 2, "memory": 1024, "disk": 500, "comments": "", "custom_fields": {} } ] }././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9659586 pynetbox-7.8.0/tests/fixtures/wireless/0000755000175100017510000000000015215017112017707 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/wireless/wireless_lan.json0000644000175100017510000000004615215017101023267 0ustar00runnerrunner{ "id": 1, "ssid": "SSID 1" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/fixtures/wireless/wireless_lans.json0000644000175100017510000000034015215017101023447 0ustar00runnerrunner{ "count": 2, "next": null, "previous": null, "results": [ { "id": 1, "ssid": "SSID 1" }, { "id": 2, "ssid": "SSID 2" } ] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9666653 pynetbox-7.8.0/tests/integration/0000755000175100017510000000000015215017112016524 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/integration/conftest.py0000644000175100017510000004637415215017101020737 0ustar00runnerrunnerimport atexit import os import subprocess as subp import time from http.client import RemoteDisconnected import pytest import requests import yaml import pynetbox DOCKER_PROJECT_PREFIX = "pytest_pynetbox" def get_netbox_docker_version_tag(netbox_version): """Get the repo tag to build netbox-docker in from the requested netbox version. Args: netbox_version (version.Version): The version of netbox we want to build Returns: str: The release tag for the netbox-docker repo that should be able to build the requested version of netbox. """ major, minor = netbox_version.major, netbox_version.minor if (major, minor) == (4, 2): tag = "3.2.1" elif (major, minor) == (4, 3): tag = "3.3.0" elif (major, minor) == (4, 4): tag = "3.4.2" elif (major, minor) == (4, 5): tag = "4.0.2" elif (major, minor) == (4, 6): tag = "5.0.1" else: raise NotImplementedError( "Version %s is not currently supported" % netbox_version ) return tag @pytest.fixture(scope="session") def git_toplevel(): """Get the top level of the current git repo. Returns: str: The path of the top level directory of the current git repo. """ try: subp.check_call(["which", "git"]) except subp.CalledProcessError: pytest.skip(reason="git executable was not found on the host") return ( subp.check_output(["git", "rev-parse", "--show-toplevel"]) .decode("utf-8") .splitlines()[0] ) @pytest.fixture(scope="session") def netbox_docker_repo_dirpaths(pytestconfig, git_toplevel): """Get the path to the netbox-docker repos we will use. Returns: dict: A map of the repo dir paths to the versions of netbox that should be run from that repo as: { : [ , ..., ] } """ try: subp.check_call(["which", "docker"]) except subp.CalledProcessError: pytest.skip(reason="docker executable was not found on the host") netbox_versions_by_repo_dirpaths = {} for netbox_version in pytestconfig.option.netbox_versions: repo_version_tag = get_netbox_docker_version_tag(netbox_version=netbox_version) print("top: ", git_toplevel) repo_fpath = os.path.join( git_toplevel, ".netbox-docker-%s" % str(repo_version_tag) ) if os.path.isdir(repo_fpath): subp.check_call( ["git", "fetch"], cwd=repo_fpath, stdout=subp.PIPE, stderr=subp.PIPE ) subp.check_call( ["git", "reset", "--hard"], cwd=repo_fpath, stdout=subp.PIPE, stderr=subp.PIPE, ) subp.check_call( ["git", "pull", "origin", "release"], cwd=repo_fpath, stdout=subp.PIPE, stderr=subp.PIPE, ) else: subp.check_call( [ "git", "clone", "https://github.com/netbox-community/netbox-docker", repo_fpath, ], cwd=git_toplevel, stdout=subp.PIPE, stderr=subp.PIPE, ) subp.check_call( ["git", "checkout", repo_version_tag], cwd=repo_fpath, stdout=subp.PIPE, stderr=subp.PIPE, ) try: netbox_versions_by_repo_dirpaths[repo_fpath].append(netbox_version) except KeyError: netbox_versions_by_repo_dirpaths[repo_fpath] = [netbox_version] return netbox_versions_by_repo_dirpaths @pytest.fixture(scope="session") def docker_compose_project_name(pytestconfig): """Get the project name to use for docker containers. This will return a consistently generated project name so we can kill stale containers after the test run is finished. """ return "%s_%s" % (DOCKER_PROJECT_PREFIX, int(time.time())) def clean_netbox_docker_tmpfiles(): """Clean up any temporary files created in the netbox-docker repo.""" dirpath, dirnames, filenames = next(os.walk("./")) for filename in filenames: if filename.startswith("docker-compose-v"): os.remove(filename) def clean_docker_objects(): """Clean up any docker objects created via these tests.""" # clean up any containers for line in subp.check_output(["docker", "ps", "-a"]).decode("utf-8").splitlines(): words = line.split() if not words: continue if words[-1].startswith(DOCKER_PROJECT_PREFIX): subp.check_call( ["docker", "rm", "-f", words[0]], stdout=subp.PIPE, stderr=subp.PIPE ) # clean up any volumes for line in ( subp.check_output(["docker", "volume", "list"]).decode("utf-8").splitlines() ): words = line.split() if not words: continue if words[-1].startswith(DOCKER_PROJECT_PREFIX): subp.check_call( ["docker", "volume", "rm", "-f", words[-1]], stdout=subp.PIPE, stderr=subp.PIPE, ) # clean up any networks for line in ( subp.check_output(["docker", "network", "list"]).decode("utf-8").splitlines() ): words = line.split() if not words: continue if words[1].startswith(DOCKER_PROJECT_PREFIX): subp.check_call( ["docker", "network", "rm", words[1]], stdout=subp.PIPE, stderr=subp.PIPE, ) # TODO: this function could be split up @pytest.fixture(scope="session") def docker_compose_file(pytestconfig, netbox_docker_repo_dirpaths): """Return paths to the compose files needed to create test containers. We can create container sets for multiple versions of netbox here by returning a list of paths to multiple compose files. """ clean_netbox_docker_tmpfiles() clean_docker_objects() compose_files = [] for ( netbox_docker_repo_dirpath, netbox_versions, ) in netbox_docker_repo_dirpaths.items(): compose_source_fpath = os.path.join( netbox_docker_repo_dirpath, "docker-compose.yml" ) for netbox_version in netbox_versions: # check for updates to the local netbox images subp.check_call( ["docker", "pull", "netboxcommunity/netbox:v%s" % (netbox_version)], stdout=subp.PIPE, stderr=subp.PIPE, ) docker_netbox_version = str(netbox_version).replace(".", "_") # load the compose file yaml compose_data = yaml.safe_load(open(compose_source_fpath, "r").read()) # add the custom network for this version docker_network_name = "%s_v%s" % ( DOCKER_PROJECT_PREFIX, docker_netbox_version, ) compose_data["networks"] = {docker_network_name: {}} # https://docs.docker.com/compose/compose-file/compose-file-v3/#network-configuration-reference if "version" not in compose_data or compose_data["version"] >= "3.5": compose_data["networks"][docker_network_name][ "name" ] = docker_network_name # prepend the netbox version to each of the service names and anything else # needed to make the continers unique to the netbox version new_services = {} for service_name in compose_data["services"].keys(): new_service_name = "netbox_v%s_%s" % ( docker_netbox_version, service_name, ) new_services[new_service_name] = compose_data["services"][service_name] if service_name in ["netbox", "netbox-worker"]: # set the netbox image version new_services[new_service_name]["image"] = ( "netboxcommunity/netbox:v%s" % netbox_version ) new_services[new_service_name]["environment"] = { "SKIP_SUPERUSER": "false", "SUPERUSER_API_TOKEN": "0123456789abcdef0123456789abcdef01234567", "SUPERUSER_EMAIL": "admin@example.com", "SUPERUSER_NAME": "admin", "SUPERUSER_PASSWORD": "admin", } if service_name == "netbox": # ensure the netbox container listens on a random port new_services[new_service_name]["ports"] = ["8080"] # Increase health check timeouts for GitHub Actions runners. # Granian (netbox-docker 4.x) binds to "::" (IPv6); try # explicit IPv4 loopback first via dual-stack, then IPv6. new_services[new_service_name]["healthcheck"] = { "test": [ "CMD-SHELL", "curl -sf http://127.0.0.1:8080/login/ || curl -sf http://[::1]:8080/login/", ], "start_period": "360s", "timeout": "10s", "interval": "15s", "retries": 10, } # set the network and an alias to the proper short name of the container # within that network new_services[new_service_name]["networks"] = { docker_network_name: {"aliases": [service_name]} } # fix the naming of any dependencies if "depends_on" in new_services[new_service_name]: depends_on = new_services[new_service_name]["depends_on"] if isinstance(depends_on, dict): # Dict form: {service: {condition: service_healthy}} — preserve conditions new_services[new_service_name]["depends_on"] = { "netbox_v%s_%s" % (docker_netbox_version, dep): cfg for dep, cfg in depends_on.items() } else: new_services[new_service_name]["depends_on"] = [ "netbox_v%s_%s" % (docker_netbox_version, dep) for dep in depends_on ] # make any internal named volumes unique to the netbox version if "volumes" in new_services[new_service_name]: new_volumes = [] for volume_config in new_services[new_service_name]["volumes"]: source = volume_config.split(":")[0] if "/" in source: if volume_config.startswith("./"): # Set the full path to the volume source. Without this # some of the containers would be spun up from the # wrong source directories. volume_source, volume_dest = volume_config.split( ":", maxsplit=1 ) volume_source = os.path.join( netbox_docker_repo_dirpath, volume_source[2::] ) new_volumes.append( ":".join([volume_source, volume_dest]) ) else: new_volumes.append(volume_config) else: new_volumes.append( "%s_v%s_%s" % ( DOCKER_PROJECT_PREFIX, docker_netbox_version, volume_config, ) ) new_services[new_service_name]["volumes"] = new_volumes # replace the services config with the renamed versions compose_data["services"] = new_services # prepend local volume names new_volumes = {} for volume_name, volume_config in compose_data["volumes"].items(): new_volumes[ "%s_v%s_%s" % ( DOCKER_PROJECT_PREFIX, docker_netbox_version, volume_name, ) ] = volume_config compose_data["volumes"] = new_volumes compose_output_fpath = os.path.join( netbox_docker_repo_dirpath, "docker-compose-v%s.yml" % netbox_version, ) with open(compose_output_fpath, "w") as fdesc: fdesc.write(yaml.dump(compose_data)) compose_files.append(compose_output_fpath) # set post=run cleanup hooks if requested if pytestconfig.option.cleanup: atexit.register(clean_docker_objects) atexit.register(clean_netbox_docker_tmpfiles) return compose_files def netbox_is_responsive(url): """Check if the HTTP service is up and responsive.""" try: response = requests.get(url) if response.status_code == 200: return True except ( ConnectionError, ConnectionResetError, requests.exceptions.ConnectionError, RemoteDisconnected, ): return False def id_netbox_service(fixture_value): """Create and ID representation for a netbox service fixture param. Returns: str: Identifiable representation of the service, as best we can """ return "netbox v%s" % fixture_value @pytest.fixture(scope="session") def docker_netbox_service( pytestconfig, docker_ip, docker_services, request, ): """Get the netbox service to test against. This function waits until the netbox container is fully up and running then does an initial data population with a few object types to be used in testing. Then the service is returned as a fixture to be called from tests. """ netbox_integration_version = request.param netbox_service_name = "netbox_v%s_netbox" % str(netbox_integration_version).replace( ".", "_" ) netbox_service_port = 8080 try: # `port_for` takes a container port and returns the corresponding host port port = docker_services.port_for(netbox_service_name, netbox_service_port) except Exception as err: docker_ps_stdout = subp.check_output(["docker", "ps", "-a"]).decode("utf-8") exited_container_logs = [] for line in docker_ps_stdout.splitlines(): if "Exited" in line: container_id = line.split()[0] exited_container_logs.append( "\nContainer %s logs:\n%s" % ( container_id, subp.check_output(["docker", "logs", container_id]).decode( "utf-8" ), ) ) raise KeyError( "Unable to find a docker service matching the name %s on port %s. Running" " containers: %s. Original error: %s. Logs:\n%s" % ( netbox_service_name, netbox_service_port, docker_ps_stdout, err, exited_container_logs, ) ) url = "http://{}:{}".format(docker_ip, port) docker_services.wait_until_responsive( timeout=300.0, pause=1, check=lambda: netbox_is_responsive(url) ) return { "url": url, "netbox_version": netbox_integration_version, } @pytest.fixture(scope="session") def api(docker_netbox_service): nb = pynetbox.api(docker_netbox_service["url"]) nb.create_token("admin", "admin") return nb @pytest.fixture(scope="session") def nb_version(docker_netbox_service): return docker_netbox_service["netbox_version"] @pytest.fixture(scope="session") def site(api): site = api.dcim.sites.create(name="test", slug="test") yield site site.delete() @pytest.fixture(scope="session") def manufacturer(api): manufacturer = api.dcim.manufacturers.create( name="test-manufacturer", slug="test-manufacturer" ) yield manufacturer manufacturer.delete() @pytest.fixture(scope="session") def device_type(api, manufacturer): device_type = api.dcim.device_types.create( manufacturer=manufacturer.id, model="test-device-type", slug="test-device-type", height=1, ) yield device_type device_type.delete() @pytest.fixture(scope="session") def role(api): role = api.dcim.device_roles.create( name="test-device-role", slug="test-device-role", color="000000", ) yield role role.delete() def create_device(api, site, device_type, role, name): """Helper function to create a device with proper version handling. Args: api: The API instance site: Site object device_type: DeviceType object role: DeviceRole object name: Device name Returns: Created device object """ from packaging import version if version.parse(api.version) >= version.parse("3.6"): return api.dcim.devices.create( name=name, role=role.id, device_type=device_type.id, site=site.id, ) else: return api.dcim.devices.create( name=name, device_role=role.id, device_type=device_type.id, site=site.id, ) def pytest_generate_tests(metafunc): """Dynamically parametrize some functions based on args from the cli parser.""" if "docker_netbox_service" in metafunc.fixturenames: # parametrize the requested versions of netbox to the docker_netbox_services fixture # so that it will return a fixture for each of the versions requested # individually rather than one fixture with multiple versions within it metafunc.parametrize( "docker_netbox_service", metafunc.config.getoption("netbox_versions"), ids=id_netbox_service, indirect=True, ) @pytest.fixture(scope="session") def docker_cleanup(pytestconfig): """Override the docker cleanup command for the containsers used in testing.""" # pytest-docker does not always clean up after itself properly, and sometimes it # will fail during cleanup because there is still a connection to one of the # running containers. Here we will disable the builtin cleanup of containers via the # pytest-docker module and implement our own instead. # This is only relevant until https://github.com/avast/pytest-docker/pull/33 gets # resolved. # There is not a great way to skip the shutdown step, so in this case to skip # it we will just pass the "version" arg so the containers are left alone command_args = "version" return command_args ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/integration/test_circuits.py0000644000175100017510000001775215215017101021774 0ustar00runnerrunnerimport pytest @pytest.fixture(scope="module") def provider(api): provider = api.circuits.providers.create(name="test-provider", slug="test-provider") yield provider provider.delete() @pytest.fixture(scope="module") def circuit_type(api): circuit_type = api.circuits.circuit_types.create( name="test-circuit-type", slug="test-circuit-type" ) yield circuit_type circuit_type.delete() @pytest.fixture(scope="module") def circuit(api, provider, circuit_type): circuit = api.circuits.circuits.create( cid="TEST-CIRCUIT-001", provider=provider.id, type=circuit_type.id ) yield circuit circuit.delete() @pytest.fixture(scope="module") def provider_network(api, provider): provider_network = api.circuits.provider_networks.create( name="test-provider-network", provider=provider.id ) yield provider_network provider_network.delete() @pytest.fixture(scope="module") def virtual_circuit_type(api): virtual_circuit_type = api.circuits.virtual_circuit_types.create( name="test-virtual-circuit-type", slug="test-virtual-circuit-type" ) yield virtual_circuit_type virtual_circuit_type.delete() @pytest.fixture(scope="module") def virtual_circuit(api, provider_network, virtual_circuit_type): virtual_circuit = api.circuits.virtual_circuits.create( cid="TEST-VCIRCUIT-001", provider_network=provider_network.id, type=virtual_circuit_type.id, ) yield virtual_circuit virtual_circuit.delete() @pytest.mark.usefixtures("init") class BaseTest: app = "circuits" def _init_helper( self, request, fixture, update_field=None, filter_kwargs=None, endpoint=None, str_repr=None, ): request.cls.endpoint = endpoint request.cls.fixture = fixture request.cls.update_field = update_field request.cls.filter_kwargs = filter_kwargs request.cls.str_repr = str_repr def test_create(self): assert self.fixture def test_str(self): if self.str_repr: test = str(self.fixture) assert test == self.str_repr def test_update_fixture(self): if self.update_field: setattr(self.fixture, self.update_field, "Test Value") assert self.fixture.save() def test_get_fixture_by_id(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(self.fixture.id) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_get_fixture_by_kwarg(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(**self.filter_kwargs) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_filter_fixture(self, api): test = list( getattr(getattr(api, self.app), self.endpoint).filter(**self.filter_kwargs) )[0] assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" class TestCircuit(BaseTest): @pytest.fixture(scope="class") def init(self, request, circuit): self._init_helper( request, circuit, filter_kwargs={"cid": circuit.cid}, update_field="description", endpoint="circuits", str_repr=circuit.cid, ) class TestCircuitTermination(BaseTest): @pytest.fixture(scope="class") def site_b(self, api): site_b = api.dcim.sites.create(name="test-site-b", slug="test-site-b") yield site_b site_b.delete() @pytest.fixture(scope="class") def circuit_termination_a(self, api, circuit, site): ret = api.circuits.circuit_terminations.create( circuit=circuit.id, site=site.id, term_side="A", termination_type="dcim.site", termination_id=site.id, ) yield ret @pytest.fixture(scope="class") def circuit_termination_b(self, api, circuit, site_b): ret = api.circuits.circuit_terminations.create( circuit=circuit.id, site=site_b.id, term_side="Z", termination_type="dcim.site", termination_id=site_b.id, ) yield ret @pytest.fixture(scope="class") def init( self, request, circuit_termination_a, ): self._init_helper( request, circuit_termination_a, filter_kwargs={"circuit_id": circuit_termination_a.circuit.id}, endpoint="circuit_terminations", str_repr=circuit_termination_a.circuit.cid, ) def test_circuit_termination_paths(self, circuit_termination_a): paths_result = circuit_termination_a.paths() assert isinstance(paths_result, list) # Circuit terminations may have paths if connected through patch panels # For this test, we just verify the method works and returns correct structure if paths_result: for path in paths_result: assert "origin" in path assert "destination" in path assert "path" in path assert isinstance(path["path"], list) class TestVirtualCircuit(BaseTest): @pytest.fixture(scope="class") def init(self, request, virtual_circuit): self._init_helper( request, virtual_circuit, filter_kwargs={"cid": virtual_circuit.cid}, update_field="description", endpoint="virtual_circuits", str_repr=virtual_circuit.cid, ) class TestVirtualCircuitTermination(BaseTest): @pytest.fixture(scope="class") def device(self, api, site, device_type, role): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-vcircuit-device") yield device device.delete() @pytest.fixture(scope="class") def interface_a(self, api, device): ret = api.dcim.interfaces.create( name="vlan100", type="virtual", device=device.id ) yield ret @pytest.fixture(scope="class") def interface_b(self, api, device): ret = api.dcim.interfaces.create( name="vlan200", type="virtual", device=device.id ) yield ret @pytest.fixture(scope="class") def virtual_circuit_termination_a(self, api, virtual_circuit, interface_a): ret = api.circuits.virtual_circuit_terminations.create( virtual_circuit=virtual_circuit.id, role="hub", interface=interface_a.id ) yield ret @pytest.fixture(scope="class") def virtual_circuit_termination_b(self, api, virtual_circuit, interface_b): ret = api.circuits.virtual_circuit_terminations.create( virtual_circuit=virtual_circuit.id, role="spoke", interface=interface_b.id ) yield ret @pytest.fixture(scope="class") def init( self, request, virtual_circuit_termination_a, ): self._init_helper( request, virtual_circuit_termination_a, filter_kwargs={ "virtual_circuit_id": virtual_circuit_termination_a.virtual_circuit.id }, endpoint="virtual_circuit_terminations", str_repr=virtual_circuit_termination_a.virtual_circuit.cid, ) def test_virtual_circuit_termination_paths(self, virtual_circuit_termination_a): paths_result = virtual_circuit_termination_a.paths() assert isinstance(paths_result, list) # Virtual circuit terminations may have paths if they traverse physical infrastructure # For this test, we just verify the method works and returns correct structure if paths_result: for path in paths_result: assert "origin" in path assert "destination" in path assert "path" in path assert isinstance(path["path"], list) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/integration/test_dcim.py0000644000175100017510000004051315215017101021052 0ustar00runnerrunnerimport pytest from packaging import version import pynetbox @pytest.fixture(scope="module") def rack(api, site): rack = api.dcim.racks.create(site=site.id, name="test-rack") yield rack rack.delete() @pytest.fixture(scope="module") def device(api, site, device_type, role): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-device") yield device device.delete() @pytest.mark.usefixtures("init") class BaseTest: app = "dcim" def _init_helper( self, request, fixture, update_field=None, filter_kwargs=None, endpoint=None, str_repr=None, ): request.cls.endpoint = endpoint request.cls.fixture = fixture request.cls.update_field = update_field request.cls.filter_kwargs = filter_kwargs request.cls.str_repr = str_repr def test_create(self): assert self.fixture def test_str(self): if self.str_repr: test = str(self.fixture) assert test == self.str_repr def test_update_fixture(self): if self.update_field: setattr(self.fixture, self.update_field, "Test Value") assert self.fixture.save() def test_get_fixture_by_id(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(self.fixture.id) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_get_fixture_by_kwarg(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(**self.filter_kwargs) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_filter_fixture(self, api): test = list( getattr(getattr(api, self.app), self.endpoint).filter(**self.filter_kwargs) )[0] assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" class TestSite(BaseTest): @pytest.fixture(scope="class") def init(self, request, site): self._init_helper( request, site, filter_kwargs={"name": "test"}, update_field="description", endpoint="sites", ) @pytest.fixture(scope="class") def add_sites(self, api): sites = api.dcim.sites.create( [ {"name": "test{}".format(i), "slug": "test{}".format(i)} for i in range(2, 20) ] ) yield for i in sites: i.delete() def test_threading_duplicates(self, docker_netbox_service, add_sites): api = pynetbox.api(docker_netbox_service["url"], threading=True) api.create_token("admin", "admin") test = api.dcim.sites.all(limit=5) test_list = list(test) test_set = set(test_list) assert len(test_list) == len(test_set) class TestRack(BaseTest): @pytest.fixture(scope="class") def init(self, request, rack): self._init_helper( request, rack, filter_kwargs={"name": rack.name}, update_field="comments", endpoint="racks", ) def test_get_elevation(self): test = self.fixture.elevation.list() assert test assert isinstance(test, list) class TestManufacturer(BaseTest): @pytest.fixture(scope="class") def init(self, request, manufacturer, nb_version): self._init_helper( request, manufacturer, filter_kwargs={"name": manufacturer.name}, update_field="description" if version.parse("2.10") < nb_version else None, endpoint="manufacturers", ) class TestDeviceType(BaseTest): @pytest.fixture(scope="class") def init(self, request, device_type): self._init_helper( request, device_type, filter_kwargs={"model": device_type.model}, update_field="comments", endpoint="device_types", str_repr=device_type.model, ) class TestDevice(BaseTest): @pytest.fixture(scope="class") def init(self, request, device): self._init_helper( request, device, filter_kwargs={"name": device.name}, update_field="comments", endpoint="devices", ) class TestInterface(BaseTest): @pytest.fixture(scope="class") def interface(self, api, device): ret = api.dcim.interfaces.create( name="test-interface", type="1000base-t", device=device.id ) yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, interface): self._init_helper( request, interface, filter_kwargs={"name": interface.name}, update_field="description", endpoint="interfaces", ) class TestPowerCable(BaseTest): @pytest.fixture(scope="class") def power_outlet(self, api, device_type, role, site): from tests.integration.conftest import create_device pdu = create_device(api, site, device_type, role, "test-pdu") outlet = api.dcim.power_outlets.create(name="outlet", device=pdu.id) yield outlet pdu.delete() @pytest.fixture(scope="class") def power_port(self, api, device): ret = api.dcim.power_ports.create(name="PSU1", device=device.id) yield ret @pytest.fixture(scope="class") def power_cable(self, api, power_outlet, power_port): cable = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.powerport", "object_id": power_port.id}, ], b_terminations=[ {"object_type": "dcim.poweroutlet", "object_id": power_outlet.id}, ], ) yield cable cable.delete() @pytest.fixture(scope="class") def init(self, request, power_cable): self._init_helper( request, power_cable, filter_kwargs={"id": power_cable.id}, endpoint="cables", str_repr="PSU1 <> outlet", ) class TestConsoleCable(BaseTest): @pytest.fixture(scope="class") def console_server_port(self, api, device_type, role, site): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-console-server") ret = api.dcim.console_server_ports.create(name="Port 1", device=device.id) yield ret device.delete() @pytest.fixture(scope="class") def console_port(self, api, device): ret = api.dcim.console_ports.create(name="Console", device=device.id) yield ret @pytest.fixture(scope="class") def console_cable(self, api, console_port, console_server_port): ret = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.consoleport", "object_id": console_port.id}, ], b_terminations=[ { "object_type": "dcim.consoleserverport", "object_id": console_server_port.id, }, ], ) yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, console_cable): self._init_helper( request, console_cable, filter_kwargs={"id": console_cable.id}, endpoint="cables", str_repr="Console <> Port 1", ) class TestInterfaceCable(BaseTest): @pytest.fixture(scope="class") def interface_b(self, api, device_type, role, site): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-device-2") ret = api.dcim.interfaces.create( name="Ethernet1", type="1000base-t", device=device.id ) yield ret device.delete() @pytest.fixture(scope="class") def interface_a(self, api, device): ret = api.dcim.interfaces.create( name="Ethernet1", type="1000base-t", device=device.id ) yield ret @pytest.fixture(scope="class") def interface_cable(self, api, interface_a, interface_b): ret = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.interface", "object_id": interface_a.id}, ], b_terminations=[ {"object_type": "dcim.interface", "object_id": interface_b.id}, ], ) yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, interface_cable): self._init_helper( request, interface_cable, filter_kwargs={"id": interface_cable.id}, endpoint="cables", str_repr="Ethernet1 <> Ethernet1", ) def test_trace(self, interface_a): test = interface_a.trace() assert test assert test[0][0].name == "Ethernet1" assert test[2][0].name == "Ethernet1" class TestPassThroughPorts(BaseTest): @pytest.fixture(scope="class") def device_a(self, api, device_type, role, site): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-device-a") yield device device.delete() @pytest.fixture(scope="class") def device_b(self, api, device_type, role, site): from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-device-b") yield device device.delete() @pytest.fixture(scope="class") def front_port_a(self, api, device_a, rear_port_a, nb_version): kwargs = dict(name="FrontPort1", device=device_a.id, type="8p8c") if version.parse(str(nb_version)) >= version.parse("4.5"): kwargs["rear_ports"] = [{"rear_port": rear_port_a.id, "rear_port_position": 1, "position": 1}] else: kwargs["rear_port"] = rear_port_a.id kwargs["rear_port_position"] = 1 ret = api.dcim.front_ports.create(**kwargs) yield ret @pytest.fixture(scope="class") def rear_port_a(self, api, device_a): ret = api.dcim.rear_ports.create( name="RearPort1", device=device_a.id, type="8p8c", positions=1 ) yield ret @pytest.fixture(scope="class") def front_port_b(self, api, device_b, rear_port_b, nb_version): kwargs = dict(name="FrontPort2", device=device_b.id, type="8p8c") if version.parse(str(nb_version)) >= version.parse("4.5"): kwargs["rear_ports"] = [{"rear_port": rear_port_b.id, "rear_port_position": 1, "position": 1}] else: kwargs["rear_port"] = rear_port_b.id kwargs["rear_port_position"] = 1 ret = api.dcim.front_ports.create(**kwargs) yield ret @pytest.fixture(scope="class") def rear_port_b(self, api, device_b): ret = api.dcim.rear_ports.create( name="RearPort2", device=device_b.id, type="8p8c", positions=1 ) yield ret @pytest.fixture(scope="class") def interface_a(self, api, device_a): ret = api.dcim.interfaces.create( name="eth0", type="1000base-t", device=device_a.id ) yield ret @pytest.fixture(scope="class") def interface_b(self, api, device_b): ret = api.dcim.interfaces.create( name="eth0", type="1000base-t", device=device_b.id ) yield ret @pytest.fixture(scope="class") def cable_front(self, api, front_port_a, front_port_b): ret = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.frontport", "object_id": front_port_a.id}, ], b_terminations=[ {"object_type": "dcim.frontport", "object_id": front_port_b.id}, ], ) yield ret ret.delete() @pytest.fixture(scope="class") def cable_rear_a(self, api, rear_port_a, interface_a): ret = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.rearport", "object_id": rear_port_a.id}, ], b_terminations=[ {"object_type": "dcim.interface", "object_id": interface_a.id}, ], ) yield ret ret.delete() @pytest.fixture(scope="class") def cable_rear_b(self, api, rear_port_b, interface_b): ret = api.dcim.cables.create( a_terminations=[ {"object_type": "dcim.rearport", "object_id": rear_port_b.id}, ], b_terminations=[ {"object_type": "dcim.interface", "object_id": interface_b.id}, ], ) yield ret ret.delete() @pytest.fixture(scope="class") def init( self, request, front_port_a, cable_front, cable_rear_a, cable_rear_b, ): self._init_helper( request, front_port_a, filter_kwargs={"name": front_port_a.name}, endpoint="front_ports", ) def test_front_port_paths(self, front_port_a): paths_result = front_port_a.paths() assert paths_result assert isinstance(paths_result, list) assert len(paths_result) > 0 # Each path should have origin, destination, and path keys for path in paths_result: assert "origin" in path assert "destination" in path assert "path" in path assert isinstance(path["path"], list) def test_rear_port_paths(self, rear_port_a): paths_result = rear_port_a.paths() assert paths_result assert isinstance(paths_result, list) class TestFrontPortRearPortsSerialization(BaseTest): """End-to-end coverage for FrontPort.rear_ports (NetBox 4.5+ list-of-mapping field, no `id` on each item). Validates: - Issue #745: re-fetched FrontPort doesn't produce a false-positive diff and save() is a no-op when nothing changed. - PR #769: in-place mutation of a rear_ports item is detected by _diff() and persisted by save(). """ @pytest.fixture(scope="class") def device(self, api, device_type, role, site, nb_version): if version.parse(str(nb_version)) < version.parse("4.5"): pytest.skip("FrontPort.rear_ports list field requires NetBox 4.5+") from tests.integration.conftest import create_device device = create_device(api, site, device_type, role, "test-device-fp-rp") yield device device.delete() @pytest.fixture(scope="class") def rear_port(self, api, device): ret = api.dcim.rear_ports.create( name="MutRearPort", device=device.id, type="8p8c", positions=4 ) yield ret @pytest.fixture(scope="class") def front_port(self, api, device, rear_port): ret = api.dcim.front_ports.create( name="MutFrontPort", device=device.id, type="8p8c", rear_ports=[ {"rear_port": rear_port.id, "rear_port_position": 1, "position": 1}, ], ) yield ret @pytest.fixture(scope="class") def init(self, request, front_port): self._init_helper( request, front_port, filter_kwargs={"name": "MutFrontPort"}, endpoint="front_ports", ) def test_save_unchanged_is_noop(self, api, front_port): # Issue #745 regression: refetch and immediately save — no false-positive # diff should be generated by the list-of-mapping rear_ports field. refetched = api.dcim.front_ports.get(front_port.id) assert refetched._diff() == set() assert refetched.save() is False def test_mutate_rear_port_position_persists(self, api, front_port): # PR #769 regression: an in-place mutation of an id-less nested # rear_ports item must be detected by _diff() and persisted by save(). refetched = api.dcim.front_ports.get(front_port.id) assert refetched.rear_ports[0].rear_port_position == 1 refetched.rear_ports[0].rear_port_position = 2 assert refetched._diff() == {"rear_ports"} assert refetched.save() is True again = api.dcim.front_ports.get(front_port.id) assert again.rear_ports[0].rear_port_position == 2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/integration/test_ipam.py0000644000175100017510000001163615215017101021070 0ustar00runnerrunnerimport pytest from packaging import version @pytest.mark.usefixtures("init") class BaseTest: app = "ipam" def _init_helper( self, request, fixture, update_field=None, filter_kwargs=None, endpoint=None, str_repr=None, ): request.cls.endpoint = endpoint request.cls.fixture = fixture request.cls.update_field = update_field request.cls.filter_kwargs = filter_kwargs request.cls.str_repr = str_repr def test_create(self): assert self.fixture def test_str(self): if self.str_repr: test = str(self.fixture) assert test == self.str_repr def test_update_fixture(self): if self.update_field: setattr(self.fixture, self.update_field, "Test Value") assert self.fixture.save() def test_get_fixture_by_id(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(self.fixture.id) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_get_fixture_by_kwarg(self, api): test = getattr(getattr(api, self.app), self.endpoint).get(**self.filter_kwargs) assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" def test_filter_fixture(self, api): test = list( getattr(getattr(api, self.app), self.endpoint).filter(**self.filter_kwargs) )[0] assert test if self.update_field: assert getattr(test, self.update_field) == "Test Value" @pytest.fixture(scope="module") def rir(api, site): ret = api.ipam.rirs.create( name="ministry-of-silly-walks", slug="ministry-of-silly-walks" ) yield ret ret.delete() class TestRIR(BaseTest): @pytest.fixture(scope="class") def init(self, request, rir, nb_version): self._init_helper( request, rir, filter_kwargs={"name": "ministry-of-silly-walks"}, update_field="description" if nb_version >= version.parse("2.8") else None, endpoint="rirs", ) class TestAggregate(BaseTest): @pytest.fixture(scope="class") def aggregate(self, api, rir): ret = api.ipam.aggregates.create(prefix="192.0.2.0/24", rir=rir.id) yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, aggregate): self._init_helper( request, aggregate, filter_kwargs={"prefix": "192.0.2.0/24"}, update_field="description", endpoint="aggregates", ) class TestPrefix(BaseTest): @pytest.fixture(scope="class") def prefix(self, api): ret = api.ipam.prefixes.create(prefix="192.0.2.0/24") yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, prefix): self._init_helper( request, prefix, filter_kwargs={"prefix": "192.0.2.0/24"}, update_field="description", endpoint="prefixes", ) class TestIpAddress(BaseTest): @pytest.fixture(scope="class") def ip(self, api): ret = api.ipam.ip_addresses.create(address="192.0.2.1/24") yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, ip): self._init_helper( request, ip, filter_kwargs={"q": "192.0.2.1/24"}, update_field="description", endpoint="ip_addresses", ) class TestRole(BaseTest): @pytest.fixture(scope="class") def role(self, api): ret = api.ipam.roles.create(name="test-role", slug="test-role") yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, role): self._init_helper( request, role, filter_kwargs={"name": "test-role"}, update_field="description", endpoint="roles", ) class TestVlan(BaseTest): @pytest.fixture(scope="class") def vlan(self, api): ret = api.ipam.vlans.create(vid=123, name="test-vlan") yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, vlan): self._init_helper( request, vlan, filter_kwargs={"name": "test-vlan"}, update_field="description", endpoint="vlans", ) class TestVRF(BaseTest): @pytest.fixture(scope="class") def vrf(self, api): ret = api.ipam.vrfs.create(name="test-vrf", rd="192.0.2.1:1234") yield ret ret.delete() @pytest.fixture(scope="class") def init(self, request, vrf): self._init_helper( request, vrf, filter_kwargs={"name": "test-vrf"}, update_field="description", endpoint="vrfs", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/integration/test_pagination.py0000644000175100017510000000634415215017101022273 0ustar00runnerrunnerimport contextlib import pytest from packaging import version import pynetbox @pytest.fixture(scope="module") def cursor_api(docker_netbox_service): """An Api instance configured for cursor pagination (NetBox 4.6+).""" nb = pynetbox.api(docker_netbox_service["url"], pagination="cursor") nb.create_token("admin", "admin") return nb @contextlib.contextmanager def capture_request_urls(api): """Record the URL of every request issued through ``api``'s session. Uses a ``requests`` response hook so the assertions can inspect exactly which query parameters were sent, rather than only the deserialized results (which look identical regardless of the pagination strategy used). """ urls = [] def _hook(response, *args, **kwargs): urls.append(response.request.url) api.http_session.hooks["response"].append(_hook) try: yield urls finally: api.http_session.hooks["response"].remove(_hook) @pytest.fixture(scope="module") def prefixes(api): """Create enough prefixes to span more than one cursor page.""" created = [ api.ipam.prefixes.create(prefix="198.51.100.{}/32".format(i)) for i in range(5) ] yield created for prefix in created: prefix.delete() class TestCursorPagination: def test_cursor_pagination_returns_all_objects( self, cursor_api, nb_version, prefixes ): """Cursor pagination walks every page and yields all objects. Gated to NetBox 4.6+, where cursor pagination is available. On older versions the client transparently falls back to offset pagination, so this end-to-end check of the ``start`` semantics and ``next`` link following is only meaningful from 4.6 onward. """ if nb_version < version.parse("4.6"): pytest.skip("cursor pagination requires NetBox 4.6+") assert cursor_api._effective_pagination() == "cursor" # Force a small page size so the result set spans multiple cursor # pages and the next-link following is actually exercised. with capture_request_urls(cursor_api) as urls: results = list(cursor_api.ipam.prefixes.filter(limit=2)) # Assert cursor pagination is genuinely in play: the offset strategy # never emits a 'start' parameter, so its presence proves the client # took the cursor path rather than silently falling back to offset. assert any("start=" in url for url in urls) returned = {str(p.prefix) for p in results} for prefix in prefixes: assert str(prefix.prefix) in returned def test_cursor_pagination_count(self, cursor_api, nb_version, prefixes): """len() on a cursor RecordSet refetches the count NetBox omits.""" if nb_version < version.parse("4.6"): pytest.skip("cursor pagination requires NetBox 4.6+") record_set = cursor_api.ipam.prefixes.filter(limit=2) with capture_request_urls(cursor_api) as urls: count = len(record_set) assert count >= len(prefixes) # len() fetches the first cursor page (start=0) before refetching the # count, so confirm the cursor path was exercised here too. assert any("start=" in url for url in urls) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_api.py0000644000175100017510000002262215215017101016365 0ustar00runnerrunnerimport concurrent.futures import unittest from unittest.mock import Mock, patch import requests import pynetbox from .util import Response host = "http://localhost:8000" def_kwargs = { "token": "abc123", } # Keys are app names, values are arbitrarily selected endpoints # We use dcim and ipam since they have unique app classes # and circuits because it does not. We don't add other apps/endpoints # beyond 'circuits' as they all use the same code as each other endpoints = { "dcim": "devices", "ipam": "prefixes", "circuits": "circuits", } class ApiTestCase(unittest.TestCase): @patch( "requests.sessions.Session.post", return_value=Response(), ) def test_get(self, *_): api = pynetbox.api(host, **def_kwargs) self.assertTrue(api) @patch( "requests.sessions.Session.post", return_value=Response(), ) def test_sanitize_url(self, *_): api = pynetbox.api("http://localhost:8000/", **def_kwargs) self.assertTrue(api) self.assertEqual(api.base_url, "http://localhost:8000/api") class ApiThreadPoolTestCase(unittest.TestCase): @patch("requests.sessions.Session.post", return_value=Response()) def test_threadpool_defaults(self, *_): api = pynetbox.api(host, **def_kwargs) self.assertIsNone(api.thread_pool_executor) self.assertEqual(api.max_workers, 4) @patch("requests.sessions.Session.post", return_value=Response()) def test_threadpool_custom_values_stored(self, *_): executor = concurrent.futures.ThreadPoolExecutor api = pynetbox.api( host, thread_pool_executor=executor, max_workers=12, **def_kwargs, ) self.assertIs(api.thread_pool_executor, executor) self.assertEqual(api.max_workers, 12) @patch("requests.sessions.Session.post", return_value=Response()) def test_threadpool_invalid_max_workers(self, *_): for invalid in (0, -1): with self.assertRaises(ValueError): pynetbox.api(host, max_workers=invalid, **def_kwargs) @patch("requests.sessions.Session.post", return_value=Response()) @patch("pynetbox.core.endpoint.Request") def test_threadpool_propagated_to_request(self, request_mock, *_): executor = concurrent.futures.ThreadPoolExecutor api = pynetbox.api( host, threading=True, thread_pool_executor=executor, max_workers=9, **def_kwargs, ) # Avoid iterating the mocked RecordSet; we only care about the kwargs # passed into the Request that backs .all(). request_mock.return_value = Mock() api.dcim.devices.all() _, kwargs = request_mock.call_args self.assertIs(kwargs["thread_pool_executor"], executor) self.assertEqual(kwargs["max_workers"], 9) self.assertTrue(kwargs["threading"]) class ApiVersionTestCase(unittest.TestCase): class ResponseHeadersWithVersion: headers = {"API-Version": "1.999"} ok = True @patch( "requests.sessions.Session.get", return_value=ResponseHeadersWithVersion(), ) def test_api_version(self, *_): api = pynetbox.api( host, ) self.assertEqual(api.version, "1.999") class ResponseHeadersWithoutVersion: headers = {} ok = True @patch( "requests.sessions.Session.get", return_value=ResponseHeadersWithoutVersion(), ) def test_api_version_not_found(self, *_): api = pynetbox.api( host, ) self.assertEqual(api.version, "") class ApiPaginationTestCase(unittest.TestCase): class ResponseHeadersWithVersion: def __init__(self, version): self.headers = {"API-Version": version} self.ok = True def test_invalid_pagination_rejected(self): with self.assertRaises(ValueError): pynetbox.api(host, pagination="bogus") def test_default_pagination_is_offset(self): api = pynetbox.api(host) self.assertEqual(api.pagination, "offset") @patch("requests.sessions.Session.get") def test_effective_pagination_offset_no_version_request(self, mock_get): """Offset mode must not probe the server version.""" api = pynetbox.api(host) self.assertEqual(api._effective_pagination(), "offset") mock_get.assert_not_called() @patch("requests.sessions.Session.get") def test_effective_pagination_cursor_deferred_until_iteration(self, mock_get): """Building a cursor-mode RecordSet must not probe the server version. The version lookup behind cursor pagination is resolved lazily on the first page fetch, so an .all()/.filter() that is never iterated makes no extra request. """ api = pynetbox.api(host, pagination="cursor") api.dcim.devices.all() api.dcim.devices.filter(name="test") mock_get.assert_not_called() def test_effective_pagination_cursor_supported(self): api = pynetbox.api(host, pagination="cursor") with patch( "requests.sessions.Session.get", return_value=self.ResponseHeadersWithVersion("4.6"), ) as mock_get: self.assertEqual(api._effective_pagination(), "cursor") # Result is cached; a second call does not re-probe. self.assertEqual(api._effective_pagination(), "cursor") self.assertEqual(mock_get.call_count, 1) def test_effective_pagination_cursor_unsupported_falls_back(self): api = pynetbox.api(host, pagination="cursor") with patch( "requests.sessions.Session.get", return_value=self.ResponseHeadersWithVersion("4.5"), ): self.assertEqual(api._effective_pagination(), "offset") def test_effective_pagination_cursor_network_error_falls_back(self): """A transport-level failure in the version probe falls back to offset.""" api = pynetbox.api(host, pagination="cursor") with patch( "requests.sessions.Session.get", side_effect=requests.exceptions.ConnectionError("boom"), ): self.assertEqual(api._effective_pagination(), "offset") def test_effective_pagination_cursor_threading_warns(self): """Confirming cursor support with threading=True warns about the no-op.""" api = pynetbox.api(host, pagination="cursor", threading=True) with patch( "requests.sessions.Session.get", return_value=self.ResponseHeadersWithVersion("4.6"), ): with self.assertWarns(UserWarning): self.assertEqual(api._effective_pagination(), "cursor") def test_effective_pagination_offset_fallback_does_not_warn(self): """Falling back to offset (NetBox < 4.6) leaves threading functional.""" import warnings api = pynetbox.api(host, pagination="cursor", threading=True) with patch( "requests.sessions.Session.get", return_value=self.ResponseHeadersWithVersion("4.5"), ): with warnings.catch_warnings(): warnings.simplefilter("error") self.assertEqual(api._effective_pagination(), "offset") class ApiStatusTestCase(unittest.TestCase): class ResponseWithStatus: ok = True def json(self): return { "netbox-version": "0.9.9", } @patch( "requests.sessions.Session.get", return_value=ResponseWithStatus(), ) def test_api_status(self, *_): api = pynetbox.api( host, ) self.assertEqual(api.status()["netbox-version"], "0.9.9") class ApiCreateTokenTestCase(unittest.TestCase): @patch( "requests.sessions.Session.post", return_value=Response(fixture="api/token_provision_v1_legacy.json"), ) def test_create_token_v1_legacy(self, *_): """Old NetBox (pre-4.5): response has only 'key' field.""" api = pynetbox.api(host) token = api.create_token("user", "pass") self.assertTrue(isinstance(token, pynetbox.core.response.Record)) self.assertEqual(token.key, "1234567890123456789012345678901234567890") self.assertEqual(api.token, "1234567890123456789012345678901234567890") @patch( "requests.sessions.Session.post", return_value=Response(fixture="api/token_provision_v1_with_token.json"), ) def test_create_token_v1_with_token(self, *_): """NetBox 4.5+ v1 tokens: response includes 'token' field; use it directly.""" api = pynetbox.api(host) token = api.create_token("user", "pass") self.assertTrue(isinstance(token, pynetbox.core.response.Record)) self.assertEqual(token.key, "1234567890123456789012345678901234567890") self.assertEqual(api.token, "plaintexttoken7890abcdef1234567890abcdef") @patch( "requests.sessions.Session.post", return_value=Response(fixture="api/token_provision_v2.json"), ) def test_create_token_v2(self, *_): """NetBox 4.5+ v2 tokens: auth token must be 'nbt_.'.""" api = pynetbox.api(host) token = api.create_token("user", "pass") self.assertTrue(isinstance(token, pynetbox.core.response.Record)) self.assertEqual(token.key, "shortkey1234567") self.assertEqual( api.token, "nbt_shortkey1234567.plaintexttoken7890abcdef1234567890abcdef" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_app.py0000644000175100017510000000504415215017101016373 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox host = "http://localhost:8000" def_kwargs = { "token": "abc123", } class AppConfigTestCase(unittest.TestCase): @patch( "pynetbox.core.query.Request.get", return_value={ "tables": { "DeviceTable": { "columns": [ "name", "status", "tenant", "tags", ], }, }, }, ) def test_config(self, *_): api = pynetbox.api(host, **def_kwargs) config = api.users.config() self.assertEqual(sorted(config.keys()), ["tables"]) self.assertEqual( sorted(config["tables"]["DeviceTable"]["columns"]), ["name", "status", "tags", "tenant"], ) class PluginAppTestCase(unittest.TestCase): @patch( "pynetbox.core.query.Request.get", return_value=[ { "name": "test_plugin", "package": "netbox_test_plugin", } ], ) def test_installed_plugins(self, *_): api = pynetbox.api(host, **def_kwargs) plugins = api.plugins.installed_plugins() self.assertEqual(len(plugins), 1) self.assertEqual(plugins[0]["name"], "test_plugin") def test_plugin_app_name(self, *_): api = pynetbox.api(host, **def_kwargs) test_plugin = api.plugins.test_plugin self.assertEqual(test_plugin.name, "plugins/test-plugin") class AppEndpointLiteralNameTestCase(unittest.TestCase): def test_attribute_access_converts_underscores(self): api = pynetbox.api(host, **def_kwargs) endpoint = api.plugins.custom_objects.my_custom_object self.assertEqual(endpoint.name, "my-custom-object") self.assertEqual( endpoint.url, "{}/api/plugins/custom-objects/my-custom-object".format(host), ) def test_endpoint_method_preserves_underscores(self): api = pynetbox.api(host, **def_kwargs) endpoint = api.plugins.custom_objects.endpoint("my_custom_object") self.assertEqual(endpoint.name, "my_custom_object") self.assertEqual( endpoint.url, "{}/api/plugins/custom-objects/my_custom_object".format(host), ) def test_endpoint_method_works_on_builtin_app(self): api = pynetbox.api(host, **def_kwargs) endpoint = api.dcim.endpoint("device_roles") self.assertEqual(endpoint.name, "device_roles") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_circuits.py0000644000175100017510000000625015215017101017440 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from .util import Response api = pynetbox.api( "http://localhost:8000", ) nb = api.circuits HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "circuits" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class CircuitsTestCase(Generic.Tests): name = "circuits" @patch( "requests.sessions.Session.get", return_value=Response(fixture="circuits/circuit.json"), ) def test_repr(self, _): test = nb.circuits.get(1) self.assertEqual(str(test), "123456") class ProviderTestCase(Generic.Tests): name = "providers" class CircuitTypeTestCase(Generic.Tests): name = "circuit_types" class CircuitTerminationsTestCase(Generic.Tests): name = "circuit_terminations" @patch( "requests.sessions.Session.get", return_value=Response(fixture="circuits/circuit_termination.json"), ) def test_repr(self, _): test = nb.circuit_terminations.get(1) self.assertEqual(str(test), "123456") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_dcim.py0000644000175100017510000000547215215017101016534 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from pynetbox.models.ipam import IpAddresses from .util import Response api = pynetbox.api( "http://localhost:8000", ) nb = api.dcim HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "dcim" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class DevicesTestCase(Generic.Tests): name = "devices" @patch( "requests.sessions.Session.get", return_value=Response(fixture="dcim/device.json"), ) def test_oob_ip_attr(self, _): device = nb.devices.get(1) self.assertIsInstance(device.oob_ip, IpAddresses) self.assertEqual(str(device.oob_ip), "192.0.2.1/32") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_tenancy.py0000644000175100017510000000504715215017101017257 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from .util import Response api = pynetbox.api( "http://localhost:8000", ) nb = api.tenancy HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "tenancy" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class TenantsTestCase(Generic.Tests): name = "tenants" class TenantGroupsTestCase(Generic.Tests): name = "tenant_groups" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_users.py0000644000175100017510000001016115215017101016750 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from .util import Response api = pynetbox.api( "http://localhost:8000", ) nb = api.users HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "users" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class UsersTestCase(Generic.Tests): name = "users" @patch( "requests.sessions.Session.get", return_value=Response(fixture="users/user.json"), ) def test_repr(self, _): test = nb.users.get(1) self.assertEqual(type(test), pynetbox.models.users.Users) self.assertEqual(str(test), "user1") class GroupsTestCase(Generic.Tests): name = "groups" class PermissionsTestCase(Generic.Tests): name = "permissions" @patch( "requests.sessions.Session.get", return_value=Response(fixture="users/permission.json"), ) def test_username(self, _): permission = nb.permissions.get(1) self.assertEqual(len(permission.users), 1) user = permission.users[0] self.assertEqual(str(user), "user1") @patch( "requests.sessions.Session.get", return_value=Response(fixture="users/permission.json"), ) def test_constraints(self, _): permission = nb.permissions.get(1) self.assertIsInstance(permission.constraints, list) self.assertIsInstance(permission.constraints[0], dict) class UnknownModelTestCase(unittest.TestCase): """This test validates that an unknown model is returned as Record object and that the __str__() method correctly uses the 'display' field of the object (introduced as a standard field in NetBox 2.11.0). """ @patch( "requests.sessions.Session.get", return_value=Response(fixture="users/unknown_model.json"), ) def test_unknown_model(self, _): unknown_obj = nb.unknown_model.get(1) self.assertEqual(type(unknown_obj), pynetbox.core.response.Record) self.assertEqual(str(unknown_obj), "Unknown object") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_virtualization.py0000644000175100017510000000720215215017101020675 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from pynetbox.models.dcim import Devices from pynetbox.models.virtualization import VirtualMachineTypes from .util import Response api = pynetbox.api( "http://localhost:8000", ) nb = api.virtualization HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "virtualization" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class ClusterTypesTestCase(Generic.Tests): name = "cluster_types" class ClusterGroupsTestCase(Generic.Tests): name = "cluster_groups" class ClustersTestCase(Generic.Tests): name = "clusters" class VirtualMachinesTestCase(Generic.Tests): name = "virtual_machines" @patch( "requests.sessions.Session.get", return_value=Response(fixture="virtualization/virtual_machine.json"), ) def test_device_attr(self, _): vm = nb.virtual_machines.get(1) self.assertIsInstance(vm.device, Devices) self.assertEqual(vm.device.name, "test-device") @patch( "requests.sessions.Session.get", return_value=Response(fixture="virtualization/virtual_machine.json"), ) def test_virtual_machine_type_attr(self, _): vm = nb.virtual_machines.get(1) self.assertIsInstance(vm.virtual_machine_type, VirtualMachineTypes) self.assertEqual(vm.virtual_machine_type.name, "Standard") class VirtualMachineTypesTestCase(Generic.Tests): name = "virtual_machine_types" class InterfacesTestCase(Generic.Tests): name = "interfaces" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/test_wireless.py0000644000175100017510000000553315215017101017453 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from .util import Response api = pynetbox.api("http://localhost:8000") nb_app = api.wireless HEADERS = {"accept": "application/json"} class Generic: class Tests(unittest.TestCase): name = "" ret = pynetbox.core.response.Record app = "wireless" def test_get_all(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb_app, self.name).all()) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"limit": 0}, json=None, headers=HEADERS, ) def test_filter(self): with patch( "requests.sessions.Session.get", return_value=Response(fixture="{}/{}.json".format(self.app, self.name)), ) as mock: ret = list(getattr(nb_app, self.name).filter(name="test")) self.assertTrue(ret) self.assertTrue(isinstance(ret[0], self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/".format( self.app, self.name.replace("_", "-") ), params={"name": "test", "limit": 0}, json=None, headers=HEADERS, ) def test_get(self): with patch( "requests.sessions.Session.get", return_value=Response( fixture="{}/{}.json".format(self.app, self.name[:-1]) ), ) as mock: ret = getattr(nb_app, self.name).get(1) self.assertTrue(ret) self.assertTrue(isinstance(ret, self.ret)) mock.assert_called_with( "http://localhost:8000/api/{}/{}/1/".format( self.app, self.name.replace("_", "-") ), params={}, json=None, headers=HEADERS, ) class WirelessLansTestCase(Generic.Tests): name = "wireless_lans" @patch( "requests.sessions.Session.get", return_value=Response(fixture="wireless/wireless_lan.json"), ) def test_repr(self, _): wireless_lan_obj = nb_app.wireless_lans.get(1) self.assertEqual(type(wireless_lan_obj), pynetbox.models.wireless.WirelessLans) self.assertEqual(str(wireless_lan_obj), "SSID 1") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1781800521.9686368 pynetbox-7.8.0/tests/unit/0000755000175100017510000000000015215017112015160 5ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/__init__.py0000644000175100017510000000000015215017101017255 0ustar00runnerrunner././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_branching_extension.py0000644000175100017510000001633715215017101022630 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox from pynetbox.extensions import BranchingExtension from pynetbox.extensions.branching import ( Branches, BranchEvents, Changes, ) from pynetbox.models.core import Jobs def make_api(): return pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension], ) class BranchingEndpointResolutionTestCase(unittest.TestCase): def test_branches_endpoint_resolves_to_branches_record(self): nb = make_api() self.assertIs(nb.plugins.branching.branches.return_obj, Branches) def test_branch_events_endpoint_resolves(self): nb = make_api() self.assertIs(nb.plugins.branching.branch_events.return_obj, BranchEvents) def test_changes_endpoint_resolves(self): nb = make_api() self.assertIs(nb.plugins.branching.changes.return_obj, Changes) def test_branches_url_slug(self): nb = make_api() # plugins/branching with underscores in the attribute name # should still produce the dashed URL slug. self.assertEqual( nb.plugins.branching.branches.url, "http://localhost:8000/api/plugins/branching/branches", ) class BranchActionsTestCase(unittest.TestCase): """Cover #710: branch.merge / .sync / .revert / .archive.""" def _branch(self, nb): with patch( "pynetbox.core.query.Request._make_call", return_value=[{"id": 1, "name": "feature-branch"}], ): return nb.plugins.branching.branches.get(1) def _patch_make_call(self, return_value): """Patch _make_call so call_args captures (self, *args, **kwargs). ``autospec=True`` binds the descriptor so the call records the bound Request instance as the first positional arg, letting us assert on the constructed URL. """ return patch( "pynetbox.core.query.Request._make_call", autospec=True, return_value=return_value, ) def test_merge_posts_to_correct_url_and_returns_job(self): nb = make_api() branch = self._branch(nb) with self._patch_make_call( {"id": 42, "name": "Merge Branch", "status": "pending"} ) as mock_call: job = branch.merge(commit=True) self.assertIsInstance(job, Jobs) self.assertEqual(job.id, 42) request_self = mock_call.call_args.args[0] self.assertEqual(mock_call.call_args.kwargs.get("verb"), "post") self.assertEqual( mock_call.call_args.kwargs.get("data"), {"commit": True, "acknowledge_conflicts": False}, ) self.assertEqual( request_self.url, "http://localhost:8000/api/plugins/branching/branches/1/merge/", ) def test_sync_posts_to_correct_url(self): nb = make_api() branch = self._branch(nb) with self._patch_make_call({"id": 7, "name": "Sync Branch"}) as mock_call: job = branch.sync(commit=False, acknowledge_conflicts=True) self.assertIsInstance(job, Jobs) request_self = mock_call.call_args.args[0] self.assertEqual( mock_call.call_args.kwargs.get("data"), {"commit": False, "acknowledge_conflicts": True}, ) self.assertTrue(request_self.url.endswith("/branches/1/sync/")) def test_revert_posts_to_correct_url(self): nb = make_api() branch = self._branch(nb) with self._patch_make_call({"id": 9, "name": "Revert Branch"}) as mock_call: job = branch.revert(commit=True) self.assertIsInstance(job, Jobs) request_self = mock_call.call_args.args[0] self.assertEqual(mock_call.call_args.kwargs.get("data"), {"commit": True}) self.assertTrue(request_self.url.endswith("/branches/1/revert/")) def test_archive_refreshes_branch(self): nb = make_api() branch = self._branch(nb) with self._patch_make_call( {"id": 1, "name": "feature-branch", "status": "archived"} ) as mock_call: result = branch.archive() self.assertIs(result, branch) self.assertEqual(str(branch.status), "archived") request_self = mock_call.call_args.args[0] self.assertEqual(mock_call.call_args.kwargs.get("verb"), "post") self.assertEqual(mock_call.call_args.kwargs.get("data"), {}) self.assertTrue(request_self.url.endswith("/branches/1/archive/")) def test_changes_delegates_to_top_level_endpoint(self): nb = make_api() branch = self._branch(nb) payload = { "id": 11, "object_type": "dcim.site", "object_id": 7, "object_repr": "site-1", "action": "update", "conflicts": [], "diff": {}, "original_data": {}, "modified_data": {}, "current_data": {}, } with self._patch_make_call([payload]) as mock_call: results = list(branch.changes) self.assertEqual(len(results), 1) self.assertIsInstance(results[0], Changes) request_self = mock_call.call_args.args[0] self.assertEqual( request_self.url, "http://localhost:8000/api/plugins/branching/changes/", ) self.assertEqual(request_self.filters, {"branch_id": 1}) class ChangeDiffJsonFieldsTestCase(unittest.TestCase): """ChangeDiff has several JSONField columns that must round-trip as dicts.""" def test_json_fields_preserved(self): nb = make_api() payload = { "id": 11, "object_type": "dcim.site", "object_id": 7, "object_repr": "site-1", "action": "update", "conflicts": ["name", "status"], "diff": {"name": {"original": "old", "modified": "new"}}, "original_data": {"name": "old", "status": "active"}, "modified_data": {"name": "new", "status": "active"}, "current_data": {"name": "old", "status": "active"}, } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): change = nb.plugins.branching.changes.get(11) self.assertIsInstance(change, Changes) self.assertEqual(change.conflicts, ["name", "status"]) self.assertEqual( change.diff, {"name": {"original": "old", "modified": "new"}} ) self.assertEqual(change.original_data, {"name": "old", "status": "active"}) self.assertEqual(change.modified_data, {"name": "new", "status": "active"}) self.assertEqual(change.current_data, {"name": "old", "status": "active"}) # And the dicts survive a serialize() round-trip. serialized = change.serialize() self.assertEqual(serialized["diff"], payload["diff"]) self.assertEqual(serialized["original_data"], payload["original_data"]) class BranchingContentTypesTestCase(unittest.TestCase): def test_content_types_registered(self): nb = make_api() self.assertIs( nb._content_type_mapper["netbox_branching.branch"], Branches ) self.assertIs( nb._content_type_mapper["netbox_branching.changediff"], Changes ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_custom_objects_extension.py0000644000175100017510000003162115215017101023711 0ustar00runnerrunner"""Unit tests for `pynetbox.extensions.custom_objects`. Covers the issue called out in #751 (``related_object_filter`` round-trips as a dict) as well as the dynamic per-type endpoint resolution and the plugin-level `schema_preview` / `schema_apply` helpers. """ import unittest from unittest.mock import Mock, patch import pynetbox from pynetbox.core.endpoint import Endpoint from pynetbox.core.response import Record from pynetbox.extensions import CustomObjectsExtension from pynetbox.extensions.custom_objects import ( CustomObject, CustomObjectTypeFields, CustomObjectTypes, LinkedObjects, schema_apply, schema_preview, ) def _api(**kwargs): kwargs.setdefault("extensions", [CustomObjectsExtension]) return pynetbox.api("http://localhost:8000", token="abc", **kwargs) class EndpointResolutionTestCase(unittest.TestCase): """Static endpoint names resolve to the shipped Record subclasses.""" def setUp(self): self.nb = _api() def test_custom_object_types_resolves(self): endpoint = self.nb.plugins.custom_objects.custom_object_types self.assertIs(endpoint.return_obj, CustomObjectTypes) self.assertTrue( endpoint.url.endswith("/plugins/custom-objects/custom-object-types") ) def test_custom_object_type_fields_resolves(self): endpoint = self.nb.plugins.custom_objects.custom_object_type_fields self.assertIs(endpoint.return_obj, CustomObjectTypeFields) def test_linked_objects_resolves(self): endpoint = self.nb.plugins.custom_objects.linked_objects self.assertIs(endpoint.return_obj, LinkedObjects) def test_unknown_endpoint_falls_back_to_custom_object(self): """Per-type endpoints aren't registered statically — they should resolve to ``CustomObject`` via the namespace's ``__getattr__``.""" endpoint = self.nb.plugins.custom_objects.cidr_list self.assertIs(endpoint.return_obj, CustomObject) self.assertTrue(endpoint.url.endswith("/plugins/custom-objects/cidr-list")) class RelatedObjectFilterRoundTripTestCase(unittest.TestCase): """Issue #751: ``related_object_filter`` should round-trip as a dict.""" PAYLOAD = { "id": 3, "name": "cidr_list", "label": "CIDR List", "custom_object_type": 1, "type": "multiobject", "related_object_type": {"id": 84, "app_label": "ipam", "model": "prefix"}, "related_object_filter": { "vrf__n": "null", "tenant_group": "aws-accounts", "mask_length__lte": 28, }, } def test_get_preserves_dict(self): with patch( "pynetbox.core.query.Request._make_call", return_value=[self.PAYLOAD] ): nb = _api() field = nb.plugins.custom_objects.custom_object_type_fields.get(3) self.assertIsInstance(field, CustomObjectTypeFields) self.assertEqual(field.related_object_filter, self.PAYLOAD["related_object_filter"]) def test_serialize_preserves_dict(self): with patch( "pynetbox.core.query.Request._make_call", return_value=[self.PAYLOAD] ): nb = _api() field = nb.plugins.custom_objects.custom_object_type_fields.get(3) # The plain serialize() output is what the user inspects (per the # bug report) and is also what the diffing layer uses to compute # updates() — both need to see the original dict. self.assertEqual( field.serialize()["related_object_filter"], self.PAYLOAD["related_object_filter"], ) def test_related_object_type_is_preserved_dict(self): with patch( "pynetbox.core.query.Request._make_call", return_value=[self.PAYLOAD] ): nb = _api() field = nb.plugins.custom_objects.custom_object_type_fields.get(3) self.assertEqual( field.related_object_type, self.PAYLOAD["related_object_type"] ) def test_polymorphic_related_object_types_preserves_data(self): """Polymorphic Object/MultiObject fields return ``related_object_types`` as a list of ``{id, app_label, model}`` dicts. pynetbox wraps each item in a base ``Record`` (JsonField doesn't apply to list values), but every attribute remains accessible.""" payload = { "id": 4, "name": "targets", "type": "multiobject", "related_object_type": None, "related_object_types": [ {"id": 84, "app_label": "ipam", "model": "prefix"}, {"id": 12, "app_label": "dcim", "model": "device"}, ], } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = _api() field = nb.plugins.custom_objects.custom_object_type_fields.get(4) self.assertEqual(len(field.related_object_types), 2) first = field.related_object_types[0] self.assertEqual(first.id, 84) self.assertEqual(first.app_label, "ipam") self.assertEqual(first.model, "prefix") class CustomObjectTypeRecordTestCase(unittest.TestCase): """`schema_document` and `fields` nesting on a CustomObjectType.""" def test_schema_document_preserved_as_dict(self): payload = { "id": 1, "name": "demo", "slug": "demo", "schema_document": { "schema_version": "1", "types": [{"slug": "demo"}], }, "fields": [], } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = _api() cot = nb.plugins.custom_objects.custom_object_types.get(1) self.assertIsInstance(cot, CustomObjectTypes) self.assertEqual(cot.schema_document, payload["schema_document"]) def test_nested_fields_become_typed_records(self): """Nested `fields` list items deserialize as CustomObjectTypeFields, so their JSON-marked columns (e.g. related_object_filter) survive a nested fetch the same way they do on the dedicated endpoint.""" payload = { "id": 1, "name": "demo", "fields": [ { "id": 3, "name": "cidr_list", "related_object_filter": {"vrf__n": "null"}, } ], } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = _api() cot = nb.plugins.custom_objects.custom_object_types.get(1) self.assertEqual(len(cot.fields), 1) nested = cot.fields[0] self.assertIsInstance(nested, CustomObjectTypeFields) self.assertEqual(nested.related_object_filter, {"vrf__n": "null"}) class CustomObjectRecordTestCase(unittest.TestCase): def test_dynamic_endpoint_returns_custom_object(self): payload = { "id": 7, "url": "http://localhost:8000/api/plugins/custom-objects/cidr-list/7/", "display": "thing", "custom_object_type": {"id": 1, "slug": "cidr-list", "name": "CIDR List"}, } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = _api() obj = nb.plugins.custom_objects.cidr_list.get(7) self.assertIsInstance(obj, CustomObject) self.assertIsInstance(obj.custom_object_type, CustomObjectTypes) self.assertEqual(obj.custom_object_type.slug, "cidr-list") def test_underscore_context_round_trips_as_dict(self): """The plugin emits ``_context`` as a nested dict when the type has context fields. The leading underscore would normally exclude it from ``extra_keys`` in ``serialize()``, but server-supplied values land in ``_init_cache`` and are serialized from there.""" payload = { "id": 7, "url": "http://localhost:8000/api/plugins/custom-objects/cidr-list/7/", "display": "thing", "name": "thing", "_context": {"summary": "192.0.2.0/24", "labels": ["a", "b"]}, } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = _api() obj = nb.plugins.custom_objects.cidr_list.get(7) self.assertIsInstance(obj._context, dict) self.assertEqual(obj._context, payload["_context"]) self.assertEqual(obj.serialize()["_context"], payload["_context"]) class LinkedObjectsTestCase(unittest.TestCase): """The linked-objects endpoint paginates like a normal NetBox list.""" def test_filter_returns_linked_objects_records(self): item = { "custom_object_type": {"id": 1, "name": "T", "slug": "t"}, "field_name": "device", "object": {"id": 5, "display": "thing", "name": "thing"}, } # The view returns {"count": N, "results": [...]} like a normal paginated list. with patch( "pynetbox.core.query.Request._make_call", return_value={"count": 1, "next": None, "previous": None, "results": [item]}, ): nb = _api() results = list( nb.plugins.custom_objects.linked_objects.filter( object_type="dcim.device", object_id=5 ) ) self.assertEqual(len(results), 1) record = results[0] self.assertIsInstance(record, LinkedObjects) # Nested fields stayed plain dicts rather than being recursed into. self.assertEqual(record.custom_object_type, item["custom_object_type"]) self.assertEqual(record.object, item["object"]) class ContentTypeMappingTestCase(unittest.TestCase): def test_content_types_registered(self): nb = _api() self.assertIs( nb._content_type_mapper["netbox_custom_objects.customobjecttype"], CustomObjectTypes, ) self.assertIs( nb._content_type_mapper["netbox_custom_objects.customobjecttypefield"], CustomObjectTypeFields, ) def test_polymorphic_resolution_uses_extension_record(self): """A generic-FK list item whose ``object_type`` matches one of the extension's content types deserializes into the extension's Record subclass.""" nb = _api() record = Record( { "id": 1, "items": [ { "object_type": "netbox_custom_objects.customobjecttypefield", "object": {"id": 9, "name": "cidr_list"}, } ], }, nb, Mock(spec=Endpoint), ) self.assertIsInstance(record.items[0].object, CustomObjectTypeFields) class SchemaHelpersTestCase(unittest.TestCase): """`schema_preview` and `schema_apply` POST to the right URL.""" SCHEMA = {"schema_version": "1", "types": [{"slug": "demo"}]} def test_schema_preview_posts_document(self): nb = _api() sentinel = {"diffs": []} with patch( "pynetbox.core.query.Request.post", return_value=sentinel ) as mock_post: result = schema_preview(nb, self.SCHEMA) self.assertIs(result, sentinel) mock_post.assert_called_once_with(self.SCHEMA) def test_schema_apply_wraps_payload(self): nb = _api() sentinel = {"applied": True, "diffs": []} with patch( "pynetbox.core.query.Request.post", return_value=sentinel ) as mock_post: result = schema_apply(nb, self.SCHEMA, allow_destructive=True) self.assertIs(result, sentinel) mock_post.assert_called_once_with( {"schema": self.SCHEMA, "allow_destructive": True} ) def test_schema_apply_defaults_allow_destructive_false(self): nb = _api() with patch( "pynetbox.core.query.Request.post", return_value={} ) as mock_post: schema_apply(nb, self.SCHEMA) payload = mock_post.call_args.args[0] self.assertFalse(payload["allow_destructive"]) def test_schema_helpers_target_plugin_url(self): """Sanity-check the URL the helper builds — easy to break, easy to forget.""" nb = _api() captured = {} def fake_request(base, **kwargs): captured.setdefault("bases", []).append(base) return Mock(post=Mock(return_value={})) with patch("pynetbox.extensions.custom_objects.Request", new=fake_request): schema_preview(nb, self.SCHEMA) schema_apply(nb, self.SCHEMA) self.assertEqual( captured["bases"], [ "http://localhost:8000/api/plugins/custom-objects/schema/preview/", "http://localhost:8000/api/plugins/custom-objects/schema/apply/", ], ) if __name__ == "__main__": unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_detailendpoint.py0000644000175100017510000001100015215017101021562 0ustar00runnerrunnerimport unittest from unittest.mock import patch import pynetbox nb = pynetbox.api("http://localhost:8000") class DetailEndpointTestCase(unittest.TestCase): def test_detail_endpoint_create_single(self): # Prefixes with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "prefix": "1.2.3.0/24"}, ): prefix_obj = nb.ipam.prefixes.get(123) self.assertEqual(prefix_obj.prefix, "1.2.3.0/24") with patch( "pynetbox.core.query.Request._make_call", return_value={"address": "1.2.3.1/24"}, ): ip_obj = prefix_obj.available_ips.create() self.assertEqual(ip_obj.address, "1.2.3.1/24") # IP Ranges with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 321, "display": "1.2.4.1-254/24"}, ): ip_range_obj = nb.ipam.ip_ranges.get(321) self.assertEqual(ip_range_obj.display, "1.2.4.1-254/24") with patch( "pynetbox.core.query.Request._make_call", return_value={"address": "1.2.4.2/24"}, ): ip_obj = ip_range_obj.available_ips.create() self.assertEqual(ip_obj.address, "1.2.4.2/24") def test_detail_endpoint_create_kwargs(self): """Detail endpoints accept kwargs in the same spirit as Endpoint.create().""" with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "prefix": "1.2.3.0/24"}, ): prefix_obj = nb.ipam.prefixes.get(123) with patch( "pynetbox.core.query.Request._make_call", return_value={"address": "1.2.3.1/24"}, ) as mock_call: ip_obj = prefix_obj.available_ips.create(description="example") mock_call.assert_called_once_with( verb="post", data={"description": "example"} ) self.assertEqual(ip_obj.address, "1.2.3.1/24") def test_detail_endpoint_create_rejects_data_and_kwargs(self): with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "prefix": "1.2.3.0/24"}, ): prefix_obj = nb.ipam.prefixes.get(123) with self.assertRaises(ValueError): prefix_obj.available_ips.create({"description": "x"}, commit=True) def test_data_source_sync(self): with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 1, "name": "test-source", "status": {"value": "new"}}, ): data_source = nb.core.data_sources.get(1) self.assertEqual(data_source.name, "test-source") self.assertTrue( data_source.sync.url.endswith("/api/core/data-sources/1/sync/") ) with patch( "pynetbox.core.query.Request._make_call", return_value={ "id": 1, "name": "test-source", "status": {"value": "queued"}, }, ) as mock_call: result = data_source.sync.create() self.assertEqual(result["status"]["value"], "queued") self.assertEqual(mock_call.call_args.kwargs["verb"], "post") def test_detail_endpoint_create_list(self): # Prefixes with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "prefix": "1.2.3.0/24"}, ): prefix_obj = nb.ipam.prefixes.get(123) self.assertEqual(prefix_obj.prefix, "1.2.3.0/24") with patch( "pynetbox.core.query.Request._make_call", return_value=[{"address": "1.2.3.1/24"}, {"address": "1.2.3.2/24"}], ): ip_list = prefix_obj.available_ips.create([{} for _ in range(2)]) self.assertTrue(isinstance(ip_list, list)) self.assertEqual(len(ip_list), 2) # IP Ranges with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 321, "display": "1.2.4.1-254/24"}, ): ip_range_obj = nb.ipam.ip_ranges.get(321) self.assertEqual(ip_range_obj.display, "1.2.4.1-254/24") with patch( "pynetbox.core.query.Request._make_call", return_value=[{"address": "1.2.4.2/24"}, {"address": "1.2.4.3/24"}], ): ip_list = ip_range_obj.available_ips.create([{} for _ in range(2)]) self.assertTrue(isinstance(ip_list, list)) self.assertEqual(len(ip_list), 2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_endpoint.py0000644000175100017510000002267615215017101020424 0ustar00runnerrunnerimport unittest from unittest.mock import Mock, patch from pynetbox.core.endpoint import Endpoint class EndPointTestCase(unittest.TestCase): def test_filter(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: api = Mock(base_url="http://localhost:8000/api", strict_filters=False) app = Mock(name="test") mock.return_value = [{"id": 123}, {"id": 321}] test_obj = Endpoint(api, app, "test") test = test_obj.filter(test="test") self.assertEqual(len(test), 2) def test_filter_invalid_pagination_args(self): api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") with self.assertRaises(ValueError) as _: test_obj.filter(offset=1) def test_filter_replace_none_with_null(self): api = Mock(base_url="http://localhost:8000/api", strict_filters=False) app = Mock(name="test") test_obj = Endpoint(api, app, "test") test = test_obj.filter(name=None, id=0) self.assertEqual(test.request.filters, {"name": "null", "id": 0}) def test_all_invalid_pagination_args(self): api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") with self.assertRaises(ValueError) as _: test_obj.all(offset=1) def test_choices(self): with patch("pynetbox.core.query.Request.options", return_value=Mock()) as mock: api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") mock.return_value = { "actions": { "POST": { "letter": { "choices": [ {"display_name": "A", "value": 1}, {"display_name": "B", "value": 2}, {"display_name": "C", "value": 3}, ] } } } } test_obj = Endpoint(api, app, "test") choices = test_obj.choices() self.assertEqual(choices["letter"][1]["display_name"], "B") self.assertEqual(choices["letter"][1]["value"], 2) def test_choices_put(self): with patch("pynetbox.core.query.Request.options", return_value=Mock()) as mock: api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") mock.return_value = { "actions": { "PUT": { "letter": { "choices": [ {"display_name": "A", "value": 1}, {"display_name": "B", "value": 2}, {"display_name": "C", "value": 3}, ] } } } } test_obj = Endpoint(api, app, "test") choices = test_obj.choices() self.assertEqual(choices["letter"][0]["display_name"], "A") self.assertEqual(choices["letter"][0]["value"], 1) def test_choices_precedence(self): with patch("pynetbox.core.query.Request.options", return_value=Mock()) as mock: api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") mock.return_value = { "actions": { "POST": { "letter": { "choices": [ {"display_name": "A", "value": 1}, {"display_name": "B", "value": 2}, {"display_name": "C", "value": 3}, ] } }, "PUT": { "letter": { "choices": [ {"display_name": "D", "value": 4}, {"display_name": "E", "value": 5}, {"display_name": "F", "value": 6}, ] } }, } } test_obj = Endpoint(api, app, "test") choices = test_obj.choices() self.assertEqual(choices["letter"][2]["display_name"], "C") self.assertEqual(choices["letter"][2]["value"], 3) def test_get_with_filter(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: mock.return_value = [{"id": 123}] api = Mock(base_url="http://localhost:8000/api", strict_filters=False) app = Mock(name="test") test_obj = Endpoint(api, app, "test") test = test_obj.get(name="test") self.assertEqual(test.id, 123) def test_delete_with_ids(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: ids = [1, 3, 5] mock.return_value = True api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") test = test_obj.delete(ids) mock.assert_called_with(verb="delete", data=[{"id": i} for i in ids]) self.assertTrue(test) def test_delete_with_objects(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: from pynetbox.core.response import Record ids = [1, 3, 5] mock.return_value = True api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") objects = [ Record({"id": i, "name": "dummy" + str(i)}, api, test_obj) for i in ids ] test = test_obj.delete(objects) mock.assert_called_with(verb="delete", data=[{"id": i} for i in ids]) self.assertTrue(test) def test_delete_with_recordset(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: from pynetbox.core.response import RecordSet ids = [1, 3, 5] class FakeRequest: def get(self): return iter([{"id": i, "name": "dummy" + str(i)} for i in ids]) mock.return_value = True api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") recordset = RecordSet(test_obj, FakeRequest()) test = test_obj.delete(recordset) mock.assert_called_with(verb="delete", data=[{"id": i} for i in ids]) self.assertTrue(test) def test_get_greater_than_one(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: mock.return_value = [{"id": 123}, {"id": 321}] api = Mock(base_url="http://localhost:8000/api", strict_filters=False) app = Mock(name="test") test_obj = Endpoint(api, app, "test") with self.assertRaises(ValueError) as _: test_obj.get(name="test") def test_get_no_results(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: mock.return_value = [] api = Mock(base_url="http://localhost:8000/api", strict_filters=False) app = Mock(name="test") test_obj = Endpoint(api, app, "test") test = test_obj.get(name="test") self.assertIsNone(test) def test_bulk_update_records(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: from pynetbox.core.response import Record ids = [1, 3, 5] mock.return_value = True api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") test_obj = Endpoint(api, app, "test") objects = [ Record( {"id": i, "name": "dummy" + str(i), "unchanged": "yes"}, api, test_obj, ) for i in ids ] for o in objects: o.name = "fluffy" + str(o.id) mock.return_value = [o.serialize() for o in objects] test = test_obj.update(objects) mock.assert_called_with( verb="patch", data=[{"id": i, "name": "fluffy" + str(i)} for i in ids] ) self.assertTrue(test) def test_bulk_update_json(self): with patch( "pynetbox.core.query.Request._make_call", return_value=Mock() ) as mock: ids = [1, 3, 5] changes = [{"id": i, "name": "puffy" + str(i)} for i in ids] mock.return_value = True api = Mock(base_url="http://localhost:8000/api") app = Mock(name="test") mock.return_value = changes test_obj = Endpoint(api, app, "test") test = test_obj.update(changes) mock.assert_called_with(verb="patch", data=changes) self.assertTrue(test) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_endpoint_strict_filter.py0000644000175100017510000000502115215017101023342 0ustar00runnerrunnerimport unittest from unittest.mock import Mock from tests.util import openapi_mock from pynetbox.core.endpoint import Endpoint from pynetbox import ParameterValidationError class StrictFilterTestCase(unittest.TestCase): def test_filter_strict_valid_kwargs(self): api = Mock( base_url="http://localhost:8000/api", strict_filters=True, openapi=openapi_mock, ) app = Mock(name="test") app.name = "test" test_obj = Endpoint(api, app, "test") test_obj.filter(test="test") def test_filter_strict_invalid_kwarg(self): api = Mock( base_url="http://localhost:8000/api", strict_filters=True, openapi=openapi_mock, ) app = Mock(name="test") app.name = "test" test_obj = Endpoint(api, app, "test") with self.assertRaises(ParameterValidationError): test_obj.filter(very_invalid_kwarg="test") def test_filter_strict_per_request_disable_invalid_kwarg(self): api = Mock( base_url="http://localhost:8000/api", strict_filters=True, # Enable globally openapi=openapi_mock, ) app = Mock(name="test") app.name = "test" test_obj = Endpoint(api, app, "test") # Disable strict_filters only for this specific request test_obj.filter(very_invalid_kwarg="test", strict_filters=False) def test_filter_strict_per_request_enable_invalid_kwarg(self): api = Mock( base_url="http://localhost:8000/api", strict_filters=False, # Disable globally openapi=openapi_mock, ) app = Mock(name="test") app.name = "test" test_obj = Endpoint(api, app, "test") with self.assertRaises(ParameterValidationError): # Enable strict_filters only for this specific request test_obj.filter(very_invalid_kwarg="test", strict_filters=True) def test_filter_strict_allows_custom_field_kwargs(self): # NetBox custom field filters (cf_) are not enumerated in # the OpenAPI spec but are valid query parameters. Strict validation # must let them through. api = Mock( base_url="http://localhost:8000/api", strict_filters=True, openapi=openapi_mock, ) app = Mock(name="test") app.name = "test" test_obj = Endpoint(api, app, "test") test_obj.filter(cf_CRM_ID="121121") test_obj.filter(cf_my_field__gt=10) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_extensions.py0000644000175100017510000002714515215017101020777 0ustar00runnerrunnerimport unittest from unittest.mock import Mock, patch import pynetbox from pynetbox.core.endpoint import DetailEndpoint, Endpoint from pynetbox.core.extension import Extension from pynetbox.core.response import JsonField, Record # Models used by the extensions defined below. Modeled after the shapes # called out in netbox-community/pynetbox#710 and #745 #751. class Branches(Record): """Plugin Record subclass with a custom DetailEndpoint, like #710.""" @property def merge(self): return DetailEndpoint(self, "merge") class BranchingModels: Branches = Branches class BranchingExtension(Extension): plugin_name = "branching" models = BranchingModels class CustomObjectTypeFields(Record): """Plugin Record marking a dict field as JSON, like #751.""" related_object_filter = JsonField class CustomObjectsModels: CustomObjectTypeFields = CustomObjectTypeFields class CustomObjectsExtension(Extension): plugin_name = "custom_objects" models = CustomObjectsModels content_types = { "custom_objects.customobjecttypefield": CustomObjectTypeFields, } class ExtensionRegistrationTestCase(unittest.TestCase): def test_extension_registry_built(self): nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension, CustomObjectsExtension], ) self.assertIn("branching", nb._extensions) self.assertIn("custom_objects", nb._extensions) def test_missing_plugin_name_raises(self): class BadExtension: models = None with self.assertRaises(ValueError): pynetbox.api( "http://localhost:8000", token="abc", extensions=[BadExtension] ) def test_duplicate_plugin_name_raises(self): class OtherBranchingExtension(Extension): plugin_name = "branching" models = BranchingModels with self.assertRaisesRegex(ValueError, "Duplicate extension"): pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension, OtherBranchingExtension], ) def test_plugin_name_dash_underscore_equivalent(self): """plugin_name 'custom-objects' and 'custom_objects' are the same key.""" class DashExtension(Extension): plugin_name = "custom-objects" models = CustomObjectsModels class UnderscoreExtension(Extension): plugin_name = "custom_objects" models = CustomObjectsModels with self.assertRaisesRegex(ValueError, "Duplicate extension"): pynetbox.api( "http://localhost:8000", token="abc", extensions=[DashExtension, UnderscoreExtension], ) def test_plugin_name_dashed_registers_under_underscored_key(self): class DashExtension(Extension): plugin_name = "access-lists" models = BranchingModels nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[DashExtension] ) self.assertIn("access_lists", nb._extensions) self.assertNotIn("access-lists", nb._extensions) def test_duplicate_content_type_across_extensions_raises(self): class ConflictingExtension(Extension): plugin_name = "other_plugin" models = CustomObjectsModels content_types = { "custom_objects.customobjecttypefield": CustomObjectTypeFields, } with self.assertRaisesRegex(ValueError, "Duplicate content_type"): pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension, ConflictingExtension], ) def test_extension_can_override_builtin_content_type(self): """Overriding a built-in CONTENT_TYPE_MAPPER entry is allowed.""" class CableOverride(Record): pass class CableOverrideExtension(Extension): plugin_name = "cable_override" models = None content_types = {"dcim.cable": CableOverride} nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CableOverrideExtension], ) self.assertIs(nb._content_type_mapper["dcim.cable"], CableOverride) def test_content_types_merged_into_mapper(self): nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension], ) self.assertIs( nb._content_type_mapper["custom_objects.customobjecttypefield"], CustomObjectTypeFields, ) # Reverse mapping is rebuilt as well. self.assertEqual( nb._type_content_mapper[CustomObjectTypeFields], "custom_objects.customobjecttypefield", ) # Built-in mappings are preserved. self.assertIn("dcim.cable", nb._content_type_mapper) class PluginEndpointResolutionTestCase(unittest.TestCase): def test_plugin_endpoint_uses_extension_record(self): """An endpoint under nb.plugins. resolves to the extension's Record.""" nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension], ) endpoint = nb.plugins.branching.branches self.assertIs(endpoint.return_obj, Branches) def test_plugin_without_extension_falls_back_to_record(self): nb = pynetbox.api("http://localhost:8000", token="abc") endpoint = nb.plugins.unknown_plugin.things self.assertIs(endpoint.return_obj, Record) def test_plugin_endpoint_with_dashed_slug(self): """Extension plugin_name 'custom_objects' matches URL slug 'custom-objects'.""" nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension], ) endpoint = nb.plugins.custom_objects.custom_object_type_fields self.assertIs(endpoint.return_obj, CustomObjectTypeFields) # URL slug should still use dashes per existing convention. self.assertEqual( endpoint.url, "http://localhost:8000/api/plugins/custom-objects/custom-object-type-fields", ) class PluginRecordBehaviorTestCase(unittest.TestCase): def test_detail_endpoint_on_plugin_record(self): """#710 shape: plugin Record exposes DetailEndpoint via @property.""" with patch("pynetbox.core.query.Request._make_call") as mock: mock.return_value = [{"id": 1, "name": "feature-branch"}] nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension], ) branch = nb.plugins.branching.branches.get(1) self.assertIsInstance(branch, Branches) self.assertIsInstance(branch.merge, DetailEndpoint) self.assertTrue(branch.merge.url.endswith("/branches/1/merge/")) def test_jsonfield_preserves_dict_on_plugin_record(self): """#751 shape: a JSON dict field on a plugin record stays a dict.""" payload = { "id": 3, "name": "cidr_list", "related_object_filter": { "vrf__n": "null", "tenant_group": "aws-accounts", "mask_length__lte": 28, }, } with patch( "pynetbox.core.query.Request._make_call", return_value=[payload] ): nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension], ) field = nb.plugins.custom_objects.custom_object_type_fields.get(3) self.assertIsInstance(field, CustomObjectTypeFields) self.assertEqual( field.related_object_filter, { "vrf__n": "null", "tenant_group": "aws-accounts", "mask_length__lte": 28, }, ) # And round-trips through serialize(). self.assertEqual( field.serialize()["related_object_filter"], { "vrf__n": "null", "tenant_group": "aws-accounts", "mask_length__lte": 28, }, ) class PluginAppPickleTestCase(unittest.TestCase): def test_plugin_app_setstate_restores_model(self): """App.__setstate__ should re-resolve the extension's models on unpickle.""" import pickle nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[BranchingExtension], ) app = nb.plugins.branching round_tripped = pickle.loads(pickle.dumps(app)) # After unpickle, the model should still resolve to BranchingModels # because _setmodel re-consults the api's extension registry. self.assertIs(round_tripped.model, BranchingModels) class GenericListExtensionTestCase(unittest.TestCase): def test_extension_record_used_for_gfk(self): """A polymorphic list item with a plugin content_type uses the extension Record.""" nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension], ) record = Record( { "id": 1, "things": [ { "object_type": "custom_objects.customobjecttypefield", "object": {"id": 9, "name": "cidr_list"}, } ], }, nb, Mock(spec=Endpoint), ) self.assertIsInstance(record.things[0].object, CustomObjectTypeFields) def test_unmapped_object_type_preserved_as_dict(self): """An unregistered plugin object_type stays as the raw dict instead of crashing. Real-world case: a NetBox instance returns generic-list items from a plugin the caller hasn't added an extension for. Parsing should not raise. """ nb = pynetbox.api("http://localhost:8000", token="abc") raw_item = { "object_type": "some_plugin.some_model", "object": {"id": 9, "name": "unknown"}, } record = Record( {"id": 1, "things": [raw_item]}, nb, Mock(spec=Endpoint), ) self.assertEqual(record.things, [raw_item]) def test_unmapped_and_mapped_object_types_mixed(self): """Mapped entries become GenericListObject; unmapped entries stay raw dicts.""" nb = pynetbox.api( "http://localhost:8000", token="abc", extensions=[CustomObjectsExtension], ) record = Record( { "id": 1, "things": [ { "object_type": "custom_objects.customobjecttypefield", "object": {"id": 9, "name": "cidr_list"}, }, { "object_type": "some_plugin.some_model", "object": {"id": 10, "name": "unknown"}, }, ], }, nb, Mock(spec=Endpoint), ) self.assertIsInstance(record.things[0].object, CustomObjectTypeFields) self.assertEqual( record.things[1], { "object_type": "some_plugin.some_model", "object": {"id": 10, "name": "unknown"}, }, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_extras.py0000644000175100017510000000235415215017101020101 0ustar00runnerrunnerimport unittest from pynetbox.models.extras import ConfigContexts class ExtrasTestCase(unittest.TestCase): def test_config_contexts(self): test_values = { "data": { "test_int": 123, "test_str": "testing", "test_list": [1, 2, 3], } } test = ConfigContexts(test_values, None, None) self.assertTrue(test) def test_config_contexts_diff_str(self): test_values = { "data": { "test_int": 123, "test_str": "testing", "test_list": [1, 2, 3], "test_dict": {"foo": "bar"}, } } test = ConfigContexts(test_values, None, None) test.data["test_str"] = "bar" self.assertEqual(test._diff(), {"data"}) def test_config_contexts_diff_dict(self): test_values = { "data": { "test_int": 123, "test_str": "testing", "test_list": [1, 2, 3], "test_dict": {"foo": "bar"}, } } test = ConfigContexts(test_values, None, None) test.data["test_dict"].update({"bar": "foo"}) self.assertEqual(test._diff(), {"data"}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_file_upload.py0000644000175100017510000002103715215017101021055 0ustar00runnerrunner"""Tests for file upload/multipart support.""" import io import unittest from unittest.mock import Mock, patch from pynetbox.core.query import Request, _extract_files, _is_file_like from pynetbox.core.endpoint import Endpoint class TestIsFileLike(unittest.TestCase): """Tests for _is_file_like helper function.""" def test_file_object(self): """File objects should be detected as file-like.""" f = io.BytesIO(b"test content") self.assertTrue(_is_file_like(f)) def test_string_io(self): """StringIO objects should be detected as file-like.""" f = io.StringIO("test content") self.assertTrue(_is_file_like(f)) def test_string(self): """Strings should not be detected as file-like.""" self.assertFalse(_is_file_like("test")) def test_bytes(self): """Bytes should not be detected as file-like.""" self.assertFalse(_is_file_like(b"test")) def test_dict(self): """Dicts should not be detected as file-like.""" self.assertFalse(_is_file_like({"key": "value"})) def test_none(self): """None should not be detected as file-like.""" self.assertFalse(_is_file_like(None)) def test_non_callable_read_attribute(self): """Objects with non-callable read attribute should not be file-like.""" class FakeFile: read = "not a method" self.assertFalse(_is_file_like(FakeFile())) class TestExtractFiles(unittest.TestCase): """Tests for _extract_files helper function.""" def test_no_files(self): """Data without files should return unchanged.""" data = {"name": "test", "device": 1} clean_data, files = _extract_files(data) self.assertEqual(clean_data, {"name": "test", "device": 1}) self.assertIsNone(files) def test_extract_file_object(self): """File objects should be extracted from data.""" file_obj = io.BytesIO(b"test content") data = {"name": "test", "image": file_obj} clean_data, files = _extract_files(data) self.assertEqual(clean_data, {"name": "test"}) self.assertIn("image", files) self.assertEqual(files["image"][0], "image") # filename defaults to key self.assertEqual(files["image"][1], file_obj) def test_extract_file_with_name_attribute(self): """File objects with name attribute should use that as filename.""" file_obj = io.BytesIO(b"test content") file_obj.name = "/path/to/image.png" data = {"name": "test", "image": file_obj} clean_data, files = _extract_files(data) self.assertEqual(files["image"][0], "image.png") # basename extracted def test_tuple_format(self): """Files passed as tuples should be preserved.""" file_obj = io.BytesIO(b"test content") data = {"name": "test", "image": ("custom_name.png", file_obj, "image/png")} clean_data, files = _extract_files(data) self.assertEqual(clean_data, {"name": "test"}) self.assertEqual(files["image"], ("custom_name.png", file_obj, "image/png")) def test_non_dict_data(self): """Non-dict data should be returned unchanged.""" data = [{"id": 1}, {"id": 2}] result_data, files = _extract_files(data) self.assertEqual(result_data, data) self.assertIsNone(files) class TestRequestWithFiles(unittest.TestCase): """Tests for Request._make_call with file uploads.""" def test_post_with_files_uses_multipart(self): """POST with files should use multipart/form-data.""" mock_session = Mock() mock_session.post.return_value.ok = True mock_session.post.return_value.status_code = 201 mock_session.post.return_value.json.return_value = {"id": 1, "name": "test"} file_obj = io.BytesIO(b"test content") req = Request( base="http://localhost:8000/api/extras/image-attachments", http_session=mock_session, token="testtoken", ) req._make_call( verb="post", data={"object_type": "dcim.device", "object_id": 1, "image": file_obj}, ) # Verify multipart was used (data= instead of json=) mock_session.post.assert_called_once() call_kwargs = mock_session.post.call_args self.assertIn("data", call_kwargs.kwargs) self.assertIn("files", call_kwargs.kwargs) self.assertNotIn("json", call_kwargs.kwargs) # Verify Content-Type was not set (requests handles it for multipart) headers = call_kwargs.kwargs["headers"] self.assertNotIn("Content-Type", headers) self.assertEqual(headers["accept"], "application/json") self.assertEqual(headers["authorization"], "Token testtoken") def test_post_without_files_uses_json(self): """POST without files should use JSON.""" mock_session = Mock() mock_session.post.return_value.ok = True mock_session.post.return_value.status_code = 201 mock_session.post.return_value.json.return_value = {"id": 1, "name": "test"} req = Request( base="http://localhost:8000/api/dcim/devices", http_session=mock_session, token="testtoken", ) req._make_call(verb="post", data={"name": "test-device", "site": 1}) mock_session.post.assert_called_once() call_kwargs = mock_session.post.call_args self.assertIn("json", call_kwargs.kwargs) self.assertNotIn("files", call_kwargs.kwargs) self.assertEqual( call_kwargs.kwargs["headers"]["Content-Type"], "application/json" ) def test_patch_with_files_uses_multipart(self): """PATCH with files should use multipart/form-data.""" mock_session = Mock() mock_session.patch.return_value.ok = True mock_session.patch.return_value.status_code = 200 mock_session.patch.return_value.json.return_value = {"id": 1, "name": "test"} file_obj = io.BytesIO(b"new content") req = Request( base="http://localhost:8000/api/extras/image-attachments", http_session=mock_session, token="testtoken", key=1, ) req._make_call(verb="patch", data={"image": file_obj}) mock_session.patch.assert_called_once() call_kwargs = mock_session.patch.call_args self.assertIn("data", call_kwargs.kwargs) self.assertIn("files", call_kwargs.kwargs) def test_patch_without_files_uses_json(self): """PATCH without files should use JSON and set Content-Type.""" mock_session = Mock() mock_session.patch.return_value.ok = True mock_session.patch.return_value.status_code = 200 mock_session.patch.return_value.json.return_value = {"id": 1, "name": "updated"} req = Request( base="http://localhost:8000/api/dcim/devices", http_session=mock_session, token="testtoken", key=1, ) req._make_call(verb="patch", data={"name": "updated-device"}) mock_session.patch.assert_called_once() call_kwargs = mock_session.patch.call_args self.assertIn("json", call_kwargs.kwargs) self.assertNotIn("files", call_kwargs.kwargs) self.assertEqual( call_kwargs.kwargs["headers"]["Content-Type"], "application/json" ) class TestEndpointCreateWithFiles(unittest.TestCase): """Tests for Endpoint.create() with file uploads.""" def test_create_image_attachment(self): """Creating image attachment should work with file objects.""" with patch("pynetbox.core.query.Request._make_call") as mock_call: mock_call.return_value = { "id": 1, "object_type": "dcim.device", "object_id": 1, "image": "/media/image-attachments/test.png", "name": "test.png", } api = Mock(base_url="http://localhost:8000/api") app = Mock(name="extras") endpoint = Endpoint(api, app, "image_attachments") file_obj = io.BytesIO(b"fake image content") endpoint.create( object_type="dcim.device", object_id=1, image=file_obj, name="test.png" ) mock_call.assert_called_once() call_kwargs = mock_call.call_args self.assertEqual(call_kwargs.kwargs["verb"], "post") data = call_kwargs.kwargs["data"] self.assertEqual(data["object_type"], "dcim.device") self.assertEqual(data["object_id"], 1) self.assertEqual(data["image"], file_obj) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_mapper.py0000644000175100017510000000122015215017101020046 0ustar00runnerrunnerimport unittest from pynetbox.models.mapper import CONTENT_TYPE_MAPPER from pynetbox.models.virtualization import VirtualMachineTypes class ContentTypeMapperTestCase(unittest.TestCase): def test_cable_bundle_registered(self): self.assertIn("dcim.cablebundle", CONTENT_TYPE_MAPPER) def test_rack_group_registered(self): self.assertIn("dcim.rackgroup", CONTENT_TYPE_MAPPER) def test_virtual_machine_type_registered(self): self.assertIn("virtualization.virtualmachinetype", CONTENT_TYPE_MAPPER) self.assertIs( CONTENT_TYPE_MAPPER["virtualization.virtualmachinetype"], VirtualMachineTypes ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1781800513.0 pynetbox-7.8.0/tests/unit/test_multiformat_endpoint.py0000644000175100017510000001210715215017101023033 0ustar00runnerrunner"""Tests for ROMultiFormatDetailEndpoint.""" import unittest from unittest.mock import patch import pynetbox class ROMultiFormatDetailEndpointTestCase(unittest.TestCase): """Test cases for ROMultiFormatDetailEndpoint class.""" def setUp(self): """Set up test fixtures.""" self.nb = pynetbox.api("http://localhost:8000", token="test-token") def test_list_returns_json_by_default(self): """list() without render parameter returns JSON data.""" with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "name": "Test Rack"}, ): rack = self.nb.dcim.racks.get(123) with patch( "pynetbox.core.query.Request._make_call", return_value=[ {"id": 1, "name": "U1"}, {"id": 2, "name": "U2"}, ], ): result = rack.elevation.list() # Should return generator that yields dict items result_list = list(result) self.assertEqual(len(result_list), 2) # Verify custom_return was applied (RUs objects) self.assertTrue(hasattr(result_list[0], "id")) def test_list_with_render_svg_returns_raw_string(self): """list(render='svg') returns raw SVG string.""" with patch( "pynetbox.core.query.Request._make_call", return_value={"id": 123, "name": "Test Rack"}, ): rack = self.nb.dcim.racks.get(123) svg_content = '' with patch( "pynetbox.core.query.Request._make_call", return_value=svg_content, ): result = rack.elevation.list(render="svg") # Should return raw string, not wrapped in list self.assertIsInstance(result, str) self.assertEqual(result, svg_content) self.assertIn("