diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..87cb9ba --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @mfiedorowicz @natm diff --git a/.github/pull_request_labeler.yaml b/.github/pull_request_labeler.yaml new file mode 100644 index 0000000..00e8dde --- /dev/null +++ b/.github/pull_request_labeler.yaml @@ -0,0 +1,54 @@ +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/docs/**/*' + +github-actions: + - changed-files: + - any-glob-to-any-file: + - '**/.github/workflows/*' + - '**/.github/workflows/**/*' + - '**/.github/dependabot.yaml' + - '**/.github/pull_request_labeler.yaml' + +github-templates: + - changed-files: + - any-glob-to-any-file: + - '**/.github/ISSUE_TEMPLATE/*' + - '**/.github/PULL_REQUEST_TEMPLATE.md' + - '**/.github/.chglog/*' + - '**/.github/.chglog/**/*' + +internal: + - changed-files: + - any-glob-to-any-file: + - '**/.flake8' + - '**/.bandit.baseline' + - '**/.gitignore' + - '**/.pre-commit-config.yaml' + - '**/MANIFEST.in' + - '**/Makefile' + - '**/CONTRIBUTING.md' + - '**/MAINTAINERS.md' + - '**/CODE_OF_CONDUCT.md' + - '**/LICENSE' + - '**/LICENSE.txt' + - '**/THIRD-PARTY-LICENSES' + - '**/.dockerignore' + - '**/.editorconfig' + - '**/setup.cfg' + +dependencies: + - changed-files: + - any-glob-to-any-file: + - '**/pyproject.toml' + - '**/poetry.lock' + - '**/requirements.txt' + +markdown: + - changed-files: + - any-glob-to-any-file: '**/*.md' + +python: + - changed-files: + - any-glob-to-any-file: '**/*.py' diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml new file mode 100644 index 0000000..f4687be --- /dev/null +++ b/.github/workflows/labeler.yaml @@ -0,0 +1,20 @@ +name: PR labeler +on: + - pull_request_target + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/labeler@v5 + with: + configuration-path: '.github/pull_request_labeler.yaml' diff --git a/.github/workflows/lint-tests.yml b/.github/workflows/lint-tests.yml new file mode 100644 index 0000000..48b7c39 --- /dev/null +++ b/.github/workflows/lint-tests.yml @@ -0,0 +1,49 @@ +name: Lint and tests +on: + workflow_dispatch: + pull_request: + push: + branches: + - "!release" + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + tests: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + python: [ "3.10", "3.11" ] + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + pip install .[dev] + pip install .[test] + + - name: Run tests with coverage + run: | + set -o pipefail + pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=netboxlabs.diode.sdk tests/ | tee pytest-coverage.txt + + - name: Pytest coverage comment + uses: MishaKav/pytest-coverage-comment@main + with: + pytest-coverage-path: ./pytest-coverage.txt + junitxml-path: ./pytest.xml + + - name: Lint with Ruff + run: | + ruff check --output-format=github netboxlabs/ tests/ + continue-on-error: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..5ddba1d --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,170 @@ +name: Release +on: + workflow_dispatch: + push: + branches: [ release ] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + GH_TOKEN: ${{ secrets.ORB_CI_GH_TOKEN }} + SEMANTIC_RELEASE_PACKAGE: ${{ github.repository }} + PYTHON_RUNTIME_VERSION: "3.11" + APP_NAME: diode-sdk-python + PYTHON_PACKAGE_NAME: netboxlabs-diode-sdk + +jobs: + get-next-version: + name: Semantic release get next version + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + - name: Write package.json + uses: DamianReeves/write-file-action@master + with: + path: ./package.json + write-mode: overwrite + contents: | + { + "name": "${{ env.APP_NAME }}", + "version": "1.0.0", + "devDependencies": { + "semantic-release-export-data": "^1.0.1", + "@semantic-release/changelog": "^6.0.3" + } + } + - name: Write .releaserc.json + uses: DamianReeves/write-file-action@master + with: + path: ./.releaserc.json + write-mode: overwrite + contents: | + { + "branches": "release", + "repositoryUrl": "https://github.com/netboxlabs/diode-sdk-python", + "debug": "true", + "tagFormat": "v${version}", + "plugins": [ + ["semantic-release-export-data"], + ["@semantic-release/commit-analyzer", { + "releaseRules": [ + { "message": "*", "release": "patch"}, + { "message": "fix*", "release": "patch" }, + { "message": "feat*", "release": "minor" }, + { "message": "perf*", "release": "major" } + ] + }], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md", + "changelogTitle": "# Semantic Versioning Changelog" + } + ], + [ + "@semantic-release/github", + { + "assets": [ + { + "path": "release/**" + } + ] + } + ] + ] + } + - name: setup semantic-release + run: npm i + - name: release dry-run + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_SEMANTIC_RELEASE_WEBHOOK }} + run: npx semantic-release --debug --dry-run + id: get-next-version + - name: Set short sha output + id: short-sha + run: echo "::set-output name=short-sha::${GITHUB_SHA::7}" + - name: Set release version + id: release-version + run: | + echo "::set-output name=release-version::`echo ${{ steps.get-next-version.outputs.new-release-version }} | sed 's/v//g'`" + outputs: + new-release-published: ${{ steps.get-next-version.outputs.new-release-published }} + new-release-version: ${{ steps.release-version.outputs.release-version }} + short-sha: ${{ steps.short-sha.outputs.short-sha }} + + confirm-version: + name: Next version ${{ needs.get-next-version.outputs.new-release-version }} + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: get-next-version + if: needs.get-next-version.outputs.new-release-published == 'true' + steps: + - uses: actions/checkout@v4 + - run: echo "The new release version is ${{ needs.get-next-version.outputs.new-release-version }} commit ${{ needs.get-next-version.outputs.short-sha }}" + + build: + name: Build + needs: [ get-next-version ] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + id-token: write + contents: read + env: + BUILD_VERSION: ${{ needs.get-next-version.outputs.new-release-version }} + BUILD_TRACK: release + BUILD_COMMIT: ${{ needs.get-next-version.outputs.short-sha }} + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_RUNTIME_VERSION }} + - name: Insert version variables into Python + run: | + sed -i "s/__commit_hash__ = .*/__commit_hash__ = \"${BUILD_COMMIT}\"/" netboxlabs/diode/sdk/version.py + sed -i "s/__track__ = .*/__track__ = \"${BUILD_TRACK}\"/" netboxlabs/diode/sdk/version.py + sed -i "s/__version__ = .*/__version__ = \"${BUILD_VERSION}\"/" netboxlabs/diode/sdk/version.py + - name: Display contents of version.py + run: cat netboxlabs/diode/sdk/version.py + - name: Build sdist package + run: | + pip install toml-cli + toml set --toml-path pyproject.toml project.version ${{ env.BUILD_VERSION }} + cat pyproject.toml | grep version + python3 -m pip install --upgrade build + python3 -m build --sdist --outdir dist/ + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PYTHON_PACKAGE_NAME }}-${{ env.BUILD_VERSION }}.tar.gz + path: ./dist/${{ env.PYTHON_PACKAGE_NAME }}-${{ env.BUILD_VERSION }}.tar.gz + retention-days: 30 + if-no-files-found: error + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ./dist + + semantic-release: + name: Semantic release + needs: [ build ] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "21.4.0" + - name: setup semantic-release + run: npm i + - name: Release + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_OBSERVABILITY_RELEASE_WEBHOOK }} + run: npx semantic-release --debug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..257791d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# IntelliJ +.idea/ + +# VS Code +.vscode + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# macOS +.DS_Store + +# Python +__pycache__/ +*.py[cod] +*$py.class +.Python +build/ +dist/ +.eggs/ +*.egg-info \ No newline at end of file diff --git a/LICENSE b/LICENSE.txt similarity index 99% rename from LICENSE rename to LICENSE.txt index 261eeb9..cd676f4 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2024 NetBox Labs, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 710c969..a765e72 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ -# diode-sdk-python -Diode SDK +# Diode SDK Python + +Diode SDK Python is a Python library for interacting with the Diode ingestion service utilizing gRPC. + +Diode is a new [NetBox](https://netboxlabs.com/oss/netbox/) ingestion service that greatly simplifies and enhances the +process to add and update network data +in NetBox, ensuring your network source of truth is always accurate and can be trusted to power your network automation +pipelines. + +More information about Diode can be found +at [https://netboxlabs.com/blog/introducing-diode-streamlining-data-ingestion-in-netbox/](https://netboxlabs.com/blog/introducing-diode-streamlining-data-ingestion-in-netbox/). + +## Installation + +```bash +pip install netboxlabs-diode-sdk +``` + +## Usage + +### Environment variables + +* `DIODE_API_KEY` - API key for the Diode service +* `DIODE_SDK_LOG_LEVEL` - Log level for the SDK (default: `INFO`) +* `DIODE_SENTRY_DSN` - Optional Sentry DSN for error reporting + +### Example + +* `target` should be the address of the Diode service, e.g. `grpc://localhost:8081` for insecure connection + or `grpcs://example.com` for secure connection. + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Device, + Entity, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Ingest device with device type, platform, manufacturer, site, role, and tags. + """ + + device = Device( + name="Device A", + device_type="Device Type A", + platform="Platform A", + manufacturer="Manufacturer A", + site="Site ABC", + role="Role ABC", + serial="123456", + asset_tag="123456", + status="active", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device=device)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Supported entities (object types) + +* [Device](./docs/entities.md#device) +* [Interface](./docs/entities.md#interface) +* [Device Type](./docs/entities.md#device-type) +* [Platform](./docs/entities.md#platform) +* [Manufacturer](./docs/entities.md#manufacturer) +* [Site](./docs/entities.md#site) +* [Role](./docs/entities.md#role) +* [IP Address](./docs/entities.md#ip-address) +* [Prefix](./docs/entities.md#prefix) + +## Development notes + +Code in `netboxlabs/diode/sdk/diode/*` is generated from Protocol Buffers definitions (will be published and referred +here soon). + +#### Linting + +```shell +ruff netboxlabs/ +black netboxlabs/ +``` + +#### Testing + +```shell +pytest tests/ +``` + +## License + +Distributed under the Apache 2.0 License. See [LICENSE.txt](./LICENSE.txt) for more information. diff --git a/docs/entities.md b/docs/entities.md new file mode 100644 index 0000000..f632abe --- /dev/null +++ b/docs/entities.md @@ -0,0 +1,749 @@ +# Supported Entities + +## Device + +Attributes: + +* `name` (str) - device name +* `device_type` (str, [DeviceType](#device-type)) - device type name or DeviceType entity +* `platform` (str, [Platform](#platform)) - platform name or Platform entity +* `manufacturer` (str, [Manufacturer](#manufacturer)) - manufacturer name or Manufacturer entity +* `site` (str, [Site](#site)) - site name or Site entity +* `role` (str, [Role](#role)) - role name or Role entity +* `serial` (str) - serial number +* `asset_tag` (str) - asset tag +* `status` (str) - status (e.g. `active`, `planned`, `staged`, `failed`, `inventory`, `decommissioning`, `offine`) +* `comments` (str) - comments +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Device, + DeviceType, + Entity, + Manufacturer, + Platform, + Role, + Site, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Device entity with only a name provided will attempt to create or update a device with + the given name and placeholders (i.e. "undefined") for other nested objects types + (e.g. DeviceType, Platform, Site, Role) required by NetBox. + """ + + device = Device(name="Device A") + + entities.append(Entity(device=device)) + + """ + Device entity using flat data structure. + """ + + device_flat = Device( + name="Device A", + device_type="Device Type A", + platform="Platform A", + manufacturer="Manufacturer A", + site="Site ABC", + role="Role ABC", + serial="123456", + asset_tag="123456", + status="active", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device=device_flat)) + + """ + Device entity using explicit data structure. + """ + + device_explicit = Device( + name="Device A", + device_type=DeviceType( + model="Device Type A", manufacturer=Manufacturer(name="Manufacturer A") + ), + platform=Platform( + name="Platform A", manufacturer=Manufacturer(name="Manufacturer A") + ), + site=Site(name="Site ABC"), + role=Role(name="Role ABC", tags=["tag 1", "tag 3"]), + serial="123456", + asset_tag="123456", + status="active", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device=device_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() +``` + +## Interface + +Attributes: + +* `name` (str) - interface name +* `device` (str, [Device](#device)) - device name or Device entity +* `device_type` (str, [DeviceType](#device-type)) - device type name or DeviceType entity +* `role` (str, [Role](#role)) - role name or Role entity +* `platform` (str, [Platform](#platform)) - platform name or Platform entity +* `manufacturer` (str, [Manufacturer](#manufacturer)) - manufacturer name or Manufacturer entity +* `site` (str, [Site](#site)) - site name or Site entity +* `type` (str) - interface type (e.g. `virtual`, `other`, etc.) +* `enabled` (bool) - is the interface enabled +* `mtu` (int) - maximum transmission unit +* `mac_address` (str) - MAC address +* `speed` (int) - speed +* `wwn` (str) - world wide name +* `mgmt_only` (bool) - is the interface for management only +* `description` (str) - description +* `mark_connected` (bool) - mark connected +* `mode` (str) - mode (`access`, `tagged`, `tagged-all`) +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Device, + DeviceType, + Entity, + Interface, + Manufacturer, + Platform, + Role, + Site, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Interface entity with only a name provided will attempt to create or update an interface with + the given name and placeholders (i.e. "undefined") for other nested objects types + (e.g. Device, DeviceType, Platform, Site, Role) required by NetBox. + """ + + interface = Interface(name="Interface A") + + entities.append(Entity(interface=interface)) + + """ + Interface entity using flat data structure. + """ + + interface_flat = Interface( + name="Interface A", + device="Device A", + device_type="Device Type A", + role="Role ABC", + platform="Platform A", + site="Site ABC", + type="virtual", + enabled=True, + mtu=1500, + mac_address="00:00:00:00:00:00", + description="Interface A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(interface=interface_flat)) + + """ + Interface entity using explicit data structure. + """ + + interface_explicit = Interface( + name="Interface A", + device=Device( + name="Device A", + device_type=DeviceType( + model="Device Type A", + manufacturer=Manufacturer(name="Manufacturer A"), + ), + platform=Platform( + name="Platform A", manufacturer=Manufacturer(name="Manufacturer A") + ), + site=Site(name="Site ABC"), + role=Role(name="Role ABC", tags=["tag 1", "tag 3"]), + ), + type="virtual", + enabled=True, + mtu=1500, + mac_address="00:00:00:00:00:00", + description="Interface A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(interface=interface_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Device Type + +Attributes: + +* `model` (str) - device type model +* `slug` (str) - slug +* `manufacturer` (str, [Manufacturer](#manufacturer)) - manufacturer name or Manufacturer entity +* `description` (str) - description +* `comments` (str) - comments +* `part_number` (str) - part number +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + DeviceType, + Entity, + Manufacturer, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + DeviceType entity with only a name provided will attempt to create or update a device type with + the given name and placeholder (i.e. "undefined") for nested Manufacturer object type + required by NetBox. + """ + + device_type = DeviceType(model="Device Type A") + + entities.append(Entity(device_type=device_type)) + + """ + DeviceType entity using flat data structure. + """ + + device_type_flat = DeviceType( + model="Device Type A", + manufacturer="Manufacturer A", + part_number="123456", + description="Device Type A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device_type=device_type_flat)) + + """ + DeviceType entity using explicit data structure. + """ + + device_type_explicit = DeviceType( + model="Device Type A", + manufacturer=Manufacturer( + name="Manufacturer A", + description="Manufacturer A description", + tags=["tag 1", "tag 2"], + ), + part_number="123456", + description="Device Type A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device_type=device_type_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Platform + +Attributes: + +* `name` (str) - platform name +* `slug` (str) - slug +* `manufacturer` (str, [Manufacturer](#manufacturer)) - manufacturer name or Manufacturer entity +* `description` (str) - description +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Entity, + Manufacturer, + Platform, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Platform entity with only a name provided will attempt to create or update a platform with + the given name and placeholders (i.e. "undefined") for other nested objects types (e.g. Manufacturer) + required by NetBox. + """ + + platform = Platform( + name="Platform A", + ) + + entities.append(Entity(platform=platform)) + + """ + Platform entity using flat data structure. + """ + + platform_flat = Platform( + name="Platform A", + manufacturer="Manufacturer A", + description="Platform A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(platform=platform_flat)) + + """ + Platform entity using explicit data structure. + """ + + platform_explicit = Platform( + name="Platform A", + manufacturer=Manufacturer(name="Manufacturer A", tags=["tag 1", "tag 3"]), + description="Platform A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(platform=platform_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Manufacturer + +Attributes: + +* `name` (str) - manufacturer name +* `slug` (str) - slug +* `description` (str) - description +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Entity, + Manufacturer, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Manufacturer entity. + """ + + manufacturer = Manufacturer( + name="Manufacturer A", + description="Manufacturer A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(manufacturer=manufacturer)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Site + +Attributes: + +* `name` (str) - site name +* `slug` (str) - slug +* `status` (str) - status (`active`, `planned`, `retired`, `staging`, `decommissioning`) +* `facility` (str) - facility +* `time_zone` (str) - time zone +* `description` (str) - description +* `comments` (str) - comments +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Entity, + Site, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Site entity. + """ + + site = Site( + name="Site A", + status="active", + facility="Data Center 1", + time_zone="UTC", + description="Site A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(site=site)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Role + +Attributes: + +* `name` (str) - role name +* `slug` (str) - slug +* `color` (str) - color +* `description` (str) - description +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Entity, + Role, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Role entity. + """ + + role = Role( + name="Role A", + slug="role-a", + color="ffffff", + description="Role A description", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(device_role=role)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## IP Address + +Attributes: + +* `address` (str) - IP address +* `interface` (str, [Interface](#interface)) - interface name or Interface entity +* `device` (str, [Device](#device)) - device name or Device entity +* `device_type` (str, [DeviceType](#device-type)) - device type name or DeviceType entity +* `device_role` (str, [Role](#role)) - device role name or Role entity +* `platform` (str, [Platform](#platform)) - platform name or Platform entity +* `manufacturer` (str, [Manufacturer](#manufacturer)) - manufacturer name or Manufacturer entity +* `site` (str, Site) - site name or Site entity +* `status` (str) - status (`active`, `reserved`, `deprecated`, `dhcp`, `slaac`) +* `role` (str) - role (`loopback`, `secondary`, `anycast`, `vip`, `vrrp`, `hsrp`, `glbp`, `carp`) +* `dns_name` (str) - DNS name +* `description` (str) - description +* `comments` (str) - comments +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Device, + DeviceType, + Entity, + Interface, + IPAddress, + Manufacturer, + Platform, + Role, + Site, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + IPAddress entity with only an address provided will attempt to create or update an IP address with + the given address and placeholders (i.e. "undefined") for other nested objects types + (e.g. Interface, Device, DeviceType, Platform, Site, Role) required by NetBox. + """ + + ip_address = IPAddress( + address="192.168.0.1/24", + ) + + entities.append(Entity(ip_address=ip_address)) + + """ + IPAddress entity using flat data structure. + """ + + ip_address_flat = IPAddress( + address="192.168.0.1/24", + interface="Interface ABC", + device="Device ABC", + device_type="Device Type ABC", + device_role="Role ABC", + platform="Platform ABC", + manufacturer="Cisco", + site="Site ABC", + status="active", + role="Role ABC", + description="IP Address A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag1", "tag2"], + ) + + entities.append(Entity(ip_address=ip_address_flat)) + + """ + IPAddress entity using explicit data structure. + """ + + ip_address_explicit = IPAddress( + address="192.168.0.1/24", + interface=Interface( + name="Interface ABC", + device=Device( + name="Device ABC", + device_type=DeviceType( + model="Device Type ABC", manufacturer=Manufacturer(name="Cisco") + ), + platform=Platform( + name="Platform ABC", manufacturer=Manufacturer(name="Cisco") + ), + site=Site(name="Site ABC"), + role=Role(name="Role ABC", tags=["tag1", "tag3"]), + ), + ), + status="active", + role="Role ABC", + description="IP Address A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag1", "tag2"], + ) + + entities.append(Entity(ip_address=ip_address_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` + +## Prefix + +Attributes: + +* `prefix` (str) - prefix +* `site` (str, [Site](#site)) - site name or Site entity +* `status` (str) - status (`active`, `reserved`, `deprecated`, `container`) +* `is_pool` (bool) - is pool +* `mark_utilized` (bool) - mark utilized +* `description` (str) - description +* `comments` (str) - comments +* `tags` (list) - tags + +### Example + +```python +from netboxlabs.diode.sdk import DiodeClient +from netboxlabs.diode.sdk.ingester import ( + Entity, + Prefix, + Site, +) + + +def main(): + with DiodeClient( + target="grpc://localhost:8081", + app_name="my-test-app", + app_version="0.0.1", + ) as client: + entities = [] + + """ + Prefix entity with only a prefix provided will attempt to create or update a prefix with + the given prefix and placeholders (i.e. "undefined") for other nested objects types (e.g. Site) + required by NetBox. + """ + + prefix = Prefix( + prefix="192.168.0.0/32", + ) + + entities.append(Entity(prefix=prefix)) + + """ + Prefix entity using flat data structure. + """ + + prefix_flat = Prefix( + prefix="192.168.0.0/32", + site="Site ABC", + status="active", + is_pool=True, + mark_utilized=True, + description="Prefix A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(prefix=prefix_flat)) + + """ + Prefix entity using explicit data structure. + """ + + prefix_explicit = Prefix( + prefix="192.168.0.0/32", + site=Site( + name="Site ABC", + status="active", + facility="Data Center 1", + time_zone="UTC", + description="Site A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ), + is_pool=True, + mark_utilized=True, + description="Prefix A description", + comments="Lorem ipsum dolor sit amet", + tags=["tag 1", "tag 2"], + ) + + entities.append(Entity(prefix=prefix_explicit)) + + response = client.ingest(entities=entities) + if response.errors: + print(f"Errors: {response.errors}") + + +if __name__ == "__main__": + main() + +``` diff --git a/netboxlabs/__init__.py b/netboxlabs/__init__.py new file mode 100644 index 0000000..0ac7d25 --- /dev/null +++ b/netboxlabs/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs - namespace.""" diff --git a/netboxlabs/diode/__init__.py b/netboxlabs/diode/__init__.py new file mode 100644 index 0000000..5219c59 --- /dev/null +++ b/netboxlabs/diode/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs - Diode namespace.""" diff --git a/netboxlabs/diode/sdk/__init__.py b/netboxlabs/diode/sdk/__init__.py new file mode 100644 index 0000000..c499df8 --- /dev/null +++ b/netboxlabs/diode/sdk/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs, Diode - SDK.""" + +from netboxlabs.diode.sdk.client import DiodeClient + +assert DiodeClient diff --git a/netboxlabs/diode/sdk/client.py b/netboxlabs/diode/sdk/client.py new file mode 100644 index 0000000..09ee255 --- /dev/null +++ b/netboxlabs/diode/sdk/client.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs, Diode - SDK - Client.""" +import collections +import logging +import os +import platform +import uuid +from collections.abc import Iterable +from urllib.parse import urlparse + +import certifi +import grpc +import sentry_sdk + +from netboxlabs.diode.sdk.diode.v1 import ingester_pb2, ingester_pb2_grpc +from netboxlabs.diode.sdk.exceptions import DiodeClientError, DiodeConfigError +from netboxlabs.diode.sdk.ingester import Entity + +_DIODE_API_KEY_ENVVAR_NAME = "DIODE_API_KEY" +_DIODE_SDK_LOG_LEVEL_ENVVAR_NAME = "DIODE_SDK_LOG_LEVEL" +_DIODE_SENTRY_DSN_ENVVAR_NAME = "DIODE_SENTRY_DSN" +_DEFAULT_STREAM = "latest" +_LOGGER = logging.getLogger(__name__) + + +def _load_certs() -> bytes: + """Loads cacert.pem.""" + with open(certifi.where(), "rb") as f: + return f.read() + + +def _get_api_key(api_key: str | None = None) -> str: + """Get API Key either from provided value or environment variable.""" + if api_key is None: + api_key = os.getenv(_DIODE_API_KEY_ENVVAR_NAME) + if api_key is None: + raise DiodeConfigError( + f"api_key param or {_DIODE_API_KEY_ENVVAR_NAME} environment variable required" + ) + return api_key + + +def parse_target(target: str) -> tuple[str, str, bool]: + """Parse the target into authority, path and tls_verify.""" + parsed_target = urlparse(target) + + if parsed_target.scheme not in ["grpc", "grpcs"]: + raise ValueError("target should start with grpc:// or grpcs://") + + tls_verify = parsed_target.scheme == "grpcs" + + authority = parsed_target.netloc + + if ":" not in authority: + authority += ":443" + + return authority, parsed_target.path, tls_verify + + +def _get_sentry_dsn(sentry_dsn: str | None = None) -> str | None: + """Get Sentry DSN either from provided value or environment variable.""" + if sentry_dsn is None: + sentry_dsn = os.getenv(_DIODE_SENTRY_DSN_ENVVAR_NAME) + return sentry_dsn + + +class DiodeClient: + """Diode Client.""" + + _name = "diode-sdk-python" + _version = "0.0.1" + _app_name = None + _app_version = None + _channel = None + _stub = None + + def __init__( + self, + target: str, + app_name: str, + app_version: str, + api_key: str | None = None, + sentry_dsn: str = None, + sentry_traces_sample_rate: float = 1.0, + sentry_profiles_sample_rate: float = 1.0, + ): + """Initiate a new client.""" + log_level = os.getenv(_DIODE_SDK_LOG_LEVEL_ENVVAR_NAME, "INFO").upper() + logging.basicConfig(level=log_level) + + self._target, self._path, self._tls_verify = parse_target(target) + self._app_name = app_name + self._app_version = app_version + self._platform = platform.platform() + self._python_version = platform.python_version() + + api_key = _get_api_key(api_key) + self._metadata = ( + ("diode-api-key", api_key), + ("platform", self._platform), + ("python-version", self._python_version), + ) + + if self._tls_verify: + _LOGGER.debug("Setting up gRPC secure channel") + self._channel = grpc.secure_channel( + self._target, + grpc.ssl_channel_credentials( + root_certificates=_load_certs(), + ), + ) + else: + _LOGGER.debug("Setting up gRPC insecure channel") + self._channel = grpc.insecure_channel( + target=self._target, + ) + + channel = self._channel + + if self._path: + _LOGGER.debug(f"Setting up gRPC interceptor for path: {self._path}") + rpc_method_interceptor = DiodeMethodClientInterceptor(subpath=self._path) + + intercept_channel = grpc.intercept_channel( + self._channel, rpc_method_interceptor + ) + channel = intercept_channel + + self._stub = ingester_pb2_grpc.IngesterServiceStub(channel) + + self._sentry_dsn = _get_sentry_dsn(sentry_dsn) + + if self._sentry_dsn is not None: + _LOGGER.debug("Setting up Sentry") + self._setup_sentry( + self._sentry_dsn, sentry_traces_sample_rate, sentry_profiles_sample_rate + ) + + @property + def name(self) -> str: + """Retrieve the name.""" + return self._name + + @property + def version(self) -> str: + """Retrieve the version.""" + return self._version + + @property + def target(self) -> str: + """Retrieve the target.""" + return self._target + + @property + def path(self) -> str: + """Retrieve the path.""" + return self._path + + @property + def tls_verify(self) -> bool: + """Retrieve the tls_verify.""" + return self._tls_verify + + @property + def app_name(self) -> str: + """Retrieve the app name.""" + return self._app_name + + @property + def app_version(self) -> str: + """Retrieve the app version.""" + return self._app_version + + @property + def channel(self) -> grpc.Channel: + """Retrieve the channel.""" + return self._channel + + def __enter__(self): + """Enters the runtime context related to the channel object.""" + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + """Exits the runtime context related to the channel object.""" + self.close() + + def close(self): + """Close the channel.""" + self._channel.close() + + def ingest( + self, + entities: Iterable[Entity | ingester_pb2.Entity | None], + stream: str | None = _DEFAULT_STREAM, + ) -> ingester_pb2.IngestResponse: + """Ingest entities.""" + try: + request = ingester_pb2.IngestRequest( + stream=stream, + id=str(uuid.uuid4()), + entities=entities, + sdk_name=self.name, + sdk_version=self.version, + producer_app_name=self.app_name, + producer_app_version=self.app_version, + ) + + return self._stub.Ingest(request, metadata=self._metadata) + except grpc.RpcError as err: + raise DiodeClientError(err) from err + + def _setup_sentry( + self, dsn: str, traces_sample_rate: float, profiles_sample_rate: float + ): + sentry_sdk.init( + dsn=dsn, + release=self.version, + traces_sample_rate=traces_sample_rate, + profiles_sample_rate=profiles_sample_rate, + ) + sentry_sdk.set_tag("target", self.target) + sentry_sdk.set_tag("path", self.path if self.path else "/") + sentry_sdk.set_tag("app_name", self.app_name) + sentry_sdk.set_tag("app_version", self.app_version) + sentry_sdk.set_tag("sdk_version", self.version) + sentry_sdk.set_tag("platform", self._platform) + sentry_sdk.set_tag("python_version", self._python_version) + + +class _ClientCallDetails( + collections.namedtuple( + "_ClientCallDetails", + ( + "method", + "timeout", + "metadata", + "credentials", + "wait_for_ready", + "compression", + ), + ), + grpc.ClientCallDetails, +): + """ + _ClientCallDetails class. + + This class describes an RPC to be invoked and is required for custom gRPC interceptors. + + """ + + pass + + +class DiodeMethodClientInterceptor( + grpc.UnaryUnaryClientInterceptor, grpc.StreamUnaryClientInterceptor +): + """ + Diode Method Client Interceptor class. + + This class is used to intercept the client calls and modify the method details. It inherits from + grpc.UnaryUnaryClientInterceptor and grpc.StreamUnaryClientInterceptor. + + Diode's default method generated from Protocol Buffers definition is /diode.v1.IngesterService/Ingest and in order + to use Diode targets with path (i.e. localhost:8081/this/is/custom/path), this interceptor is used to modify the + method details, by prepending the generated method name with the path extracted from initial target. + + """ + + def __init__(self, subpath): + """Initiate a new interceptor.""" + self._subpath = subpath + + def _intercept_call(self, continuation, client_call_details, request_or_iterator): + """Intercept call.""" + method = client_call_details.method + if client_call_details.method is not None: + method = f"{self._subpath}{client_call_details.method}" + + client_call_details = _ClientCallDetails( + method, + client_call_details.timeout, + client_call_details.metadata, + client_call_details.credentials, + client_call_details.wait_for_ready, + client_call_details.compression, + ) + + response = continuation(client_call_details, request_or_iterator) + return response + + def intercept_unary_unary(self, continuation, client_call_details, request): + """Intercept unary unary.""" + return self._intercept_call(continuation, client_call_details, request) + + def intercept_stream_unary( + self, continuation, client_call_details, request_iterator + ): + """Intercept stream unary.""" + return self._intercept_call(continuation, client_call_details, request_iterator) diff --git a/netboxlabs/diode/sdk/diode/__init__.py b/netboxlabs/diode/sdk/diode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netboxlabs/diode/sdk/diode/v1/__init__.py b/netboxlabs/diode/sdk/diode/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netboxlabs/diode/sdk/diode/v1/ingester_pb2.py b/netboxlabs/diode/sdk/diode/v1/ingester_pb2.py new file mode 100644 index 0000000..1803c77 --- /dev/null +++ b/netboxlabs/diode/sdk/diode/v1/ingester_pb2.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: diode/v1/ingester.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 +from netboxlabs.diode.sdk.validate import validate_pb2 as validate_dot_validate__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x64iode/v1/ingester.proto\x12\x08\x64iode.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17validate/validate.proto\"\x85\x05\n\x06\x44\x65vice\x12\x1b\n\x04name\x18\x01 \x01(\tB\x07\xfa\x42\x04r\x02\x18@R\x04name\x12+\n\x0b\x64\x65vice_fqdn\x18\x02 \x01(\tB\n\xfa\x42\x07r\x05\x10\x01\x18\xff\x01R\ndeviceFqdn\x12\x35\n\x0b\x64\x65vice_type\x18\x03 \x01(\x0b\x32\x14.diode.v1.DeviceTypeR\ndeviceType\x12\"\n\x04role\x18\x04 \x01(\x0b\x32\x0e.diode.v1.RoleR\x04role\x12.\n\x08platform\x18\x05 \x01(\x0b\x32\x12.diode.v1.PlatformR\x08platform\x12\x1f\n\x06serial\x18\x06 \x01(\tB\x07\xfa\x42\x04r\x02\x18\x32R\x06serial\x12\"\n\x04site\x18\x07 \x01(\x0b\x32\x0e.diode.v1.SiteR\x04site\x12%\n\tasset_tag\x18\x08 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x08\x61ssetTag\x12\x63\n\x06status\x18\t \x01(\tBK\xfa\x42HrFR\x07offlineR\x06\x61\x63tiveR\x07plannedR\x06stagedR\x06\x66\x61iledR\tinventoryR\x0f\x64\x65\x63ommissioningR\x06status\x12*\n\x0b\x64\x65scription\x18\n \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12\x1a\n\x08\x63omments\x18\x0b \x01(\tR\x08\x63omments\x12!\n\x04tags\x18\x0c \x03(\x0b\x32\r.diode.v1.TagR\x04tags\x12\x34\n\x0bprimary_ip4\x18\r \x01(\x0b\x32\x13.diode.v1.IPAddressR\nprimaryIp4\x12\x34\n\x0bprimary_ip6\x18\x0e \x01(\x0b\x32\x13.diode.v1.IPAddressR\nprimaryIp6\"\xd1\x10\n\tInterface\x12\x32\n\x06\x64\x65vice\x18\x01 \x01(\x0b\x32\x10.diode.v1.DeviceB\x08\xfa\x42\x05\xa2\x01\x02\x08\x01R\x06\x64\x65vice\x12\x1d\n\x04name\x18\x02 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18@R\x04name\x12\x1f\n\x05label\x18\x03 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18@R\x05label\x12\xfa\x0c\n\x04type\x18\x04 \x01(\tB\xe5\x0c\xfa\x42\xe1\x0cr\xde\x0cR\x07virtualR\x06\x62ridgeR\x03lagR\n100base-fxR\x0b\x31\x30\x30\x62\x61se-lfxR\n100base-txR\n100base-t1R\n1000base-tR\x0f\x31\x30\x30\x30\x62\x61se-x-gbicR\x0e\x31\x30\x30\x30\x62\x61se-x-sfpR\n2.5gbase-tR\x08\x35gbase-tR\t10gbase-tR\x0b\x31\x30gbase-cx4R\x0e\x31\x30gbase-x-sfppR\r10gbase-x-xfpR\x10\x31\x30gbase-x-xenpakR\x0c\x31\x30gbase-x-x2R\x0f\x32\x35gbase-x-sfp28R\x0f\x35\x30gbase-x-sfp56R\x0f\x34\x30gbase-x-qsfppR\x0f\x35\x30gbase-x-sfp28R\x0e\x31\x30\x30gbase-x-cfpR\x0f\x31\x30\x30gbase-x-cfp2R\x0f\x31\x30\x30gbase-x-cfp4R\x0e\x31\x30\x30gbase-x-cxpR\x0f\x31\x30\x30gbase-x-cpakR\x0f\x31\x30\x30gbase-x-dsfpR\x10\x31\x30\x30gbase-x-sfpddR\x11\x31\x30\x30gbase-x-qsfp28R\x11\x31\x30\x30gbase-x-qsfpddR\x0f\x32\x30\x30gbase-x-cfp2R\x11\x32\x30\x30gbase-x-qsfp56R\x11\x32\x30\x30gbase-x-qsfpddR\x0f\x34\x30\x30gbase-x-cfp2R\x12\x34\x30\x30gbase-x-qsfp112R\x11\x34\x30\x30gbase-x-qsfpddR\x0f\x34\x30\x30gbase-x-osfpR\x13\x34\x30\x30gbase-x-osfp-rhsR\x0f\x34\x30\x30gbase-x-cdfpR\x0f\x34\x30\x30gbase-x-cfp8R\x11\x38\x30\x30gbase-x-qsfpddR\x0f\x38\x30\x30gbase-x-osfpR\x0b\x31\x30\x30\x30\x62\x61se-kxR\n10gbase-krR\x0b\x31\x30gbase-kx4R\n25gbase-krR\x0b\x34\x30gbase-kr4R\n50gbase-krR\x0c\x31\x30\x30gbase-kp4R\x0c\x31\x30\x30gbase-kr2R\x0c\x31\x30\x30gbase-kr4R\x0bieee802.11aR\x0bieee802.11gR\x0bieee802.11nR\x0cieee802.11acR\x0cieee802.11adR\x0cieee802.11axR\x0cieee802.11ayR\x0cieee802.15.1R\x0eother-wirelessR\x03gsmR\x04\x63\x64maR\x03lteR\tsonet-oc3R\nsonet-oc12R\nsonet-oc48R\x0bsonet-oc192R\x0bsonet-oc768R\x0csonet-oc1920R\x0csonet-oc3840R\x08\x31gfc-sfpR\x08\x32gfc-sfpR\x08\x34gfc-sfpR\t8gfc-sfppR\n16gfc-sfppR\x0b\x33\x32gfc-sfp28R\x0b\x36\x34gfc-qsfppR\r128gfc-qsfp28R\x0einfiniband-sdrR\x0einfiniband-ddrR\x0einfiniband-qdrR\x10infiniband-fdr10R\x0einfiniband-fdrR\x0einfiniband-edrR\x0einfiniband-hdrR\x0einfiniband-ndrR\x0einfiniband-xdrR\x02t1R\x02\x65\x31R\x02t3R\x02\x65\x33R\x04xdslR\x06\x64ocsisR\x04gponR\x06xg-ponR\x07xgs-ponR\x07ng-pon2R\x04\x65ponR\x08\x31\x30g-eponR\x0f\x63isco-stackwiseR\x14\x63isco-stackwise-plusR\x0f\x63isco-flexstackR\x14\x63isco-flexstack-plusR\x12\x63isco-stackwise-80R\x13\x63isco-stackwise-160R\x13\x63isco-stackwise-320R\x13\x63isco-stackwise-480R\x12\x63isco-stackwise-1tR\x0bjuniper-vcpR\x13\x65xtreme-summitstackR\x17\x65xtreme-summitstack-128R\x17\x65xtreme-summitstack-256R\x17\x65xtreme-summitstack-512R\x05otherR\x04type\x12\x18\n\x07\x65nabled\x18\x05 \x01(\x08R\x07\x65nabled\x12\x1d\n\x03mtu\x18\x06 \x01(\x05\x42\x0b\xfa\x42\x08\x1a\x06\x18\x80\x80\x04(\x01R\x03mtu\x12\x1f\n\x0bmac_address\x18\x07 \x01(\tR\nmacAddress\x12\x1d\n\x05speed\x18\x08 \x01(\x05\x42\x07\xfa\x42\x04\x1a\x02(\x00R\x05speed\x12\x10\n\x03wwn\x18\t \x01(\tR\x03wwn\x12\x1b\n\tmgmt_only\x18\n \x01(\x08R\x08mgmtOnly\x12*\n\x0b\x64\x65scription\x18\x0b \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12%\n\x0emark_connected\x18\x0c \x01(\x08R\rmarkConnected\x12\x35\n\x04mode\x18\r \x01(\tB!\xfa\x42\x1er\x1cR\x06\x61\x63\x63\x65ssR\x06taggedR\ntagged-allR\x04mode\x12!\n\x04tags\x18\x0e \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xd3\x03\n\tIPAddress\x12!\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x07\xfa\x42\x04r\x02p\x01R\x07\x61\x64\x64ress\x12\x33\n\tinterface\x18\x02 \x01(\x0b\x32\x13.diode.v1.InterfaceH\x00R\tinterface\x12H\n\x06status\x18\x03 \x01(\tB0\xfa\x42-r+R\x06\x61\x63tiveR\x08reservedR\ndeprecatedR\x04\x64hcpR\x05slaacR\x06status\x12T\n\x04role\x18\x04 \x01(\tB@\xfa\x42=r;R\x08loopbackR\tsecondaryR\x07\x61nycastR\x03vipR\x04vrrpR\x04hsrpR\x04glbpR\x04\x63\x61rpR\x04role\x12P\n\x08\x64ns_name\x18\x05 \x01(\tB5\xfa\x42\x32r0\x18\xff\x01\x32+^([0-9A-Za-z_-]+|\\*)(\\.[0-9A-Za-z_-]+)*\\.?$R\x07\x64nsName\x12*\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12\x1a\n\x08\x63omments\x18\x07 \x01(\tR\x08\x63omments\x12!\n\x04tags\x18\x08 \x03(\x0b\x32\r.diode.v1.TagR\x04tagsB\x11\n\x0f\x61ssigned_object\"\xaf\x02\n\nDeviceType\x12\x1f\n\x05model\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x05model\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12:\n\x0cmanufacturer\x18\x03 \x01(\x0b\x32\x16.diode.v1.ManufacturerR\x0cmanufacturer\x12*\n\x0b\x64\x65scription\x18\x04 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12\x1a\n\x08\x63omments\x18\x05 \x01(\tR\x08\x63omments\x12(\n\x0bpart_number\x18\x06 \x01(\tB\x07\xfa\x42\x04r\x02\x18\x32R\npartNumber\x12!\n\x04tags\x18\x07 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xad\x01\n\x0cManufacturer\x12\x1d\n\x04name\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x04name\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12*\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12!\n\x04tags\x18\x04 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xe5\x01\n\x08Platform\x12\x1d\n\x04name\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x04name\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12:\n\x0cmanufacturer\x18\x03 \x01(\x0b\x32\x16.diode.v1.ManufacturerR\x0cmanufacturer\x12*\n\x0b\x64\x65scription\x18\x04 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12!\n\x04tags\x18\x05 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xbe\x02\n\x06Prefix\x12\x1f\n\x06prefix\x18\x01 \x01(\tB\x07\xfa\x42\x04r\x02p\x01R\x06prefix\x12\"\n\x04site\x18\x02 \x01(\x0b\x32\x0e.diode.v1.SiteR\x04site\x12\x46\n\x06status\x18\x03 \x01(\tB.\xfa\x42+r)R\x06\x61\x63tiveR\tcontainerR\x08reservedR\ndeprecatedR\x06status\x12\x17\n\x07is_pool\x18\x04 \x01(\x08R\x06isPool\x12#\n\rmark_utilized\x18\x05 \x01(\x08R\x0cmarkUtilized\x12*\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12\x1a\n\x08\x63omments\x18\x07 \x01(\tR\x08\x63omments\x12!\n\x04tags\x18\x08 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xd5\x01\n\x04Role\x12\x1d\n\x04name\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x04name\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12.\n\x05\x63olor\x18\x03 \x01(\tB\x18\xfa\x42\x15r\x13\x10\x06\x18\x06\x32\r^[0-9a-f]{6}$R\x05\x63olor\x12*\n\x0b\x64\x65scription\x18\x04 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12!\n\x04tags\x18\x05 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\xd6\x02\n\x04Site\x12\x1d\n\x04name\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x04name\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12Q\n\x06status\x18\x03 \x01(\tB9\xfa\x42\x36r4R\x07plannedR\x07stagingR\x06\x61\x63tiveR\x0f\x64\x65\x63ommissioningR\x07retiredR\x06status\x12#\n\x08\x66\x61\x63ility\x18\x04 \x01(\tB\x07\xfa\x42\x04r\x02\x18\x32R\x08\x66\x61\x63ility\x12\x1b\n\ttime_zone\x18\x05 \x01(\tR\x08timeZone\x12*\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x08\xfa\x42\x05r\x03\x18\xc8\x01R\x0b\x64\x65scription\x12\x1a\n\x08\x63omments\x18\x07 \x01(\tR\x08\x63omments\x12!\n\x04tags\x18\x08 \x03(\x0b\x32\r.diode.v1.TagR\x04tags\"\x85\x01\n\x03Tag\x12\x1d\n\x04name\x18\x01 \x01(\tB\t\xfa\x42\x06r\x04\x10\x01\x18\x64R\x04name\x12/\n\x04slug\x18\x02 \x01(\tB\x1b\xfa\x42\x18r\x16\x10\x01\x18\x64\x32\x10^[-a-zA-Z0-9_]+$R\x04slug\x12.\n\x05\x63olor\x18\x03 \x01(\tB\x18\xfa\x42\x15r\x13\x10\x06\x18\x06\x32\r^[0-9a-f]{6}$R\x05\x63olor\"\x9d\x04\n\x06\x45ntity\x12$\n\x04site\x18\x01 \x01(\x0b\x32\x0e.diode.v1.SiteH\x00R\x04site\x12\x30\n\x08platform\x18\x02 \x01(\x0b\x32\x12.diode.v1.PlatformH\x00R\x08platform\x12<\n\x0cmanufacturer\x18\x03 \x01(\x0b\x32\x16.diode.v1.ManufacturerH\x00R\x0cmanufacturer\x12*\n\x06\x64\x65vice\x18\x04 \x01(\x0b\x32\x10.diode.v1.DeviceH\x00R\x06\x64\x65vice\x12\x31\n\x0b\x64\x65vice_role\x18\x05 \x01(\x0b\x32\x0e.diode.v1.RoleH\x00R\ndeviceRole\x12\x37\n\x0b\x64\x65vice_type\x18\x06 \x01(\x0b\x32\x14.diode.v1.DeviceTypeH\x00R\ndeviceType\x12\x33\n\tinterface\x18\x07 \x01(\x0b\x32\x13.diode.v1.InterfaceH\x00R\tinterface\x12\x34\n\nip_address\x18\t \x01(\x0b\x32\x13.diode.v1.IPAddressH\x00R\tipAddress\x12*\n\x06prefix\x18\n \x01(\x0b\x32\x10.diode.v1.PrefixH\x00R\x06prefix\x12\x44\n\ttimestamp\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\n\xfa\x42\x07\xb2\x01\x04\x08\x01\x38\x01R\ttimestampB\x08\n\x06\x65ntity\"\xe4\x02\n\rIngestRequest\x12\"\n\x06stream\x18\x01 \x01(\tB\n\xfa\x42\x07r\x05\x10\x01\x18\xff\x01R\x06stream\x12\x39\n\x08\x65ntities\x18\x02 \x03(\x0b\x32\x10.diode.v1.EntityB\x0b\xfa\x42\x08\x92\x01\x05\x08\x01\x10\xe8\x07R\x08\x65ntities\x12\x18\n\x02id\x18\x03 \x01(\tB\x08\xfa\x42\x05r\x03\xb0\x01\x01R\x02id\x12\x36\n\x11producer_app_name\x18\x04 \x01(\tB\n\xfa\x42\x07r\x05\x10\x01\x18\xff\x01R\x0fproducerAppName\x12<\n\x14producer_app_version\x18\x05 \x01(\tB\n\xfa\x42\x07r\x05\x10\x01\x18\xff\x01R\x12producerAppVersion\x12%\n\x08sdk_name\x18\x06 \x01(\tB\n\xfa\x42\x07r\x05\x10\x01\x18\xff\x01R\x07sdkName\x12=\n\x0bsdk_version\x18\x07 \x01(\tB\x1c\xfa\x42\x19r\x17\x32\x15^(\\d)+\\.(\\d)+\\.(\\d)+$R\nsdkVersion\"(\n\x0eIngestResponse\x12\x16\n\x06\x65rrors\x18\x01 \x03(\tR\x06\x65rrors2P\n\x0fIngesterService\x12=\n\x06Ingest\x12\x17.diode.v1.IngestRequest\x1a\x18.diode.v1.IngestResponse\"\x00\x42;Z9github.com/netboxlabs/diode/diode-sdk-go/diode/v1/diodepbb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'diode.v1.ingester_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/netboxlabs/diode/diode-sdk-go/diode/v1/diodepb' + _globals['_DEVICE'].fields_by_name['name']._loaded_options = None + _globals['_DEVICE'].fields_by_name['name']._serialized_options = b'\372B\004r\002\030@' + _globals['_DEVICE'].fields_by_name['device_fqdn']._loaded_options = None + _globals['_DEVICE'].fields_by_name['device_fqdn']._serialized_options = b'\372B\007r\005\020\001\030\377\001' + _globals['_DEVICE'].fields_by_name['serial']._loaded_options = None + _globals['_DEVICE'].fields_by_name['serial']._serialized_options = b'\372B\004r\002\0302' + _globals['_DEVICE'].fields_by_name['asset_tag']._loaded_options = None + _globals['_DEVICE'].fields_by_name['asset_tag']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_DEVICE'].fields_by_name['status']._loaded_options = None + _globals['_DEVICE'].fields_by_name['status']._serialized_options = b'\372BHrFR\007offlineR\006activeR\007plannedR\006stagedR\006failedR\tinventoryR\017decommissioning' + _globals['_DEVICE'].fields_by_name['description']._loaded_options = None + _globals['_DEVICE'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_INTERFACE'].fields_by_name['device']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['device']._serialized_options = b'\372B\005\242\001\002\010\001' + _globals['_INTERFACE'].fields_by_name['name']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030@' + _globals['_INTERFACE'].fields_by_name['label']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['label']._serialized_options = b'\372B\006r\004\020\001\030@' + _globals['_INTERFACE'].fields_by_name['type']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['type']._serialized_options = b'\372B\341\014r\336\014R\007virtualR\006bridgeR\003lagR\n100base-fxR\013100base-lfxR\n100base-txR\n100base-t1R\n1000base-tR\0171000base-x-gbicR\0161000base-x-sfpR\n2.5gbase-tR\0105gbase-tR\t10gbase-tR\01310gbase-cx4R\01610gbase-x-sfppR\r10gbase-x-xfpR\02010gbase-x-xenpakR\01410gbase-x-x2R\01725gbase-x-sfp28R\01750gbase-x-sfp56R\01740gbase-x-qsfppR\01750gbase-x-sfp28R\016100gbase-x-cfpR\017100gbase-x-cfp2R\017100gbase-x-cfp4R\016100gbase-x-cxpR\017100gbase-x-cpakR\017100gbase-x-dsfpR\020100gbase-x-sfpddR\021100gbase-x-qsfp28R\021100gbase-x-qsfpddR\017200gbase-x-cfp2R\021200gbase-x-qsfp56R\021200gbase-x-qsfpddR\017400gbase-x-cfp2R\022400gbase-x-qsfp112R\021400gbase-x-qsfpddR\017400gbase-x-osfpR\023400gbase-x-osfp-rhsR\017400gbase-x-cdfpR\017400gbase-x-cfp8R\021800gbase-x-qsfpddR\017800gbase-x-osfpR\0131000base-kxR\n10gbase-krR\01310gbase-kx4R\n25gbase-krR\01340gbase-kr4R\n50gbase-krR\014100gbase-kp4R\014100gbase-kr2R\014100gbase-kr4R\013ieee802.11aR\013ieee802.11gR\013ieee802.11nR\014ieee802.11acR\014ieee802.11adR\014ieee802.11axR\014ieee802.11ayR\014ieee802.15.1R\016other-wirelessR\003gsmR\004cdmaR\003lteR\tsonet-oc3R\nsonet-oc12R\nsonet-oc48R\013sonet-oc192R\013sonet-oc768R\014sonet-oc1920R\014sonet-oc3840R\0101gfc-sfpR\0102gfc-sfpR\0104gfc-sfpR\t8gfc-sfppR\n16gfc-sfppR\01332gfc-sfp28R\01364gfc-qsfppR\r128gfc-qsfp28R\016infiniband-sdrR\016infiniband-ddrR\016infiniband-qdrR\020infiniband-fdr10R\016infiniband-fdrR\016infiniband-edrR\016infiniband-hdrR\016infiniband-ndrR\016infiniband-xdrR\002t1R\002e1R\002t3R\002e3R\004xdslR\006docsisR\004gponR\006xg-ponR\007xgs-ponR\007ng-pon2R\004eponR\01010g-eponR\017cisco-stackwiseR\024cisco-stackwise-plusR\017cisco-flexstackR\024cisco-flexstack-plusR\022cisco-stackwise-80R\023cisco-stackwise-160R\023cisco-stackwise-320R\023cisco-stackwise-480R\022cisco-stackwise-1tR\013juniper-vcpR\023extreme-summitstackR\027extreme-summitstack-128R\027extreme-summitstack-256R\027extreme-summitstack-512R\005other' + _globals['_INTERFACE'].fields_by_name['mtu']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['mtu']._serialized_options = b'\372B\010\032\006\030\200\200\004(\001' + _globals['_INTERFACE'].fields_by_name['speed']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['speed']._serialized_options = b'\372B\004\032\002(\000' + _globals['_INTERFACE'].fields_by_name['description']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_INTERFACE'].fields_by_name['mode']._loaded_options = None + _globals['_INTERFACE'].fields_by_name['mode']._serialized_options = b'\372B\036r\034R\006accessR\006taggedR\ntagged-all' + _globals['_IPADDRESS'].fields_by_name['address']._loaded_options = None + _globals['_IPADDRESS'].fields_by_name['address']._serialized_options = b'\372B\004r\002p\001' + _globals['_IPADDRESS'].fields_by_name['status']._loaded_options = None + _globals['_IPADDRESS'].fields_by_name['status']._serialized_options = b'\372B-r+R\006activeR\010reservedR\ndeprecatedR\004dhcpR\005slaac' + _globals['_IPADDRESS'].fields_by_name['role']._loaded_options = None + _globals['_IPADDRESS'].fields_by_name['role']._serialized_options = b'\372B=r;R\010loopbackR\tsecondaryR\007anycastR\003vipR\004vrrpR\004hsrpR\004glbpR\004carp' + _globals['_IPADDRESS'].fields_by_name['dns_name']._loaded_options = None + _globals['_IPADDRESS'].fields_by_name['dns_name']._serialized_options = b'\372B2r0\030\377\0012+^([0-9A-Za-z_-]+|\\*)(\\.[0-9A-Za-z_-]+)*\\.?$' + _globals['_IPADDRESS'].fields_by_name['description']._loaded_options = None + _globals['_IPADDRESS'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_DEVICETYPE'].fields_by_name['model']._loaded_options = None + _globals['_DEVICETYPE'].fields_by_name['model']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_DEVICETYPE'].fields_by_name['slug']._loaded_options = None + _globals['_DEVICETYPE'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_DEVICETYPE'].fields_by_name['description']._loaded_options = None + _globals['_DEVICETYPE'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_DEVICETYPE'].fields_by_name['part_number']._loaded_options = None + _globals['_DEVICETYPE'].fields_by_name['part_number']._serialized_options = b'\372B\004r\002\0302' + _globals['_MANUFACTURER'].fields_by_name['name']._loaded_options = None + _globals['_MANUFACTURER'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_MANUFACTURER'].fields_by_name['slug']._loaded_options = None + _globals['_MANUFACTURER'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_MANUFACTURER'].fields_by_name['description']._loaded_options = None + _globals['_MANUFACTURER'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_PLATFORM'].fields_by_name['name']._loaded_options = None + _globals['_PLATFORM'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_PLATFORM'].fields_by_name['slug']._loaded_options = None + _globals['_PLATFORM'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_PLATFORM'].fields_by_name['description']._loaded_options = None + _globals['_PLATFORM'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_PREFIX'].fields_by_name['prefix']._loaded_options = None + _globals['_PREFIX'].fields_by_name['prefix']._serialized_options = b'\372B\004r\002p\001' + _globals['_PREFIX'].fields_by_name['status']._loaded_options = None + _globals['_PREFIX'].fields_by_name['status']._serialized_options = b'\372B+r)R\006activeR\tcontainerR\010reservedR\ndeprecated' + _globals['_PREFIX'].fields_by_name['description']._loaded_options = None + _globals['_PREFIX'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_ROLE'].fields_by_name['name']._loaded_options = None + _globals['_ROLE'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_ROLE'].fields_by_name['slug']._loaded_options = None + _globals['_ROLE'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_ROLE'].fields_by_name['color']._loaded_options = None + _globals['_ROLE'].fields_by_name['color']._serialized_options = b'\372B\025r\023\020\006\030\0062\r^[0-9a-f]{6}$' + _globals['_ROLE'].fields_by_name['description']._loaded_options = None + _globals['_ROLE'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_SITE'].fields_by_name['name']._loaded_options = None + _globals['_SITE'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_SITE'].fields_by_name['slug']._loaded_options = None + _globals['_SITE'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_SITE'].fields_by_name['status']._loaded_options = None + _globals['_SITE'].fields_by_name['status']._serialized_options = b'\372B6r4R\007plannedR\007stagingR\006activeR\017decommissioningR\007retired' + _globals['_SITE'].fields_by_name['facility']._loaded_options = None + _globals['_SITE'].fields_by_name['facility']._serialized_options = b'\372B\004r\002\0302' + _globals['_SITE'].fields_by_name['description']._loaded_options = None + _globals['_SITE'].fields_by_name['description']._serialized_options = b'\372B\005r\003\030\310\001' + _globals['_TAG'].fields_by_name['name']._loaded_options = None + _globals['_TAG'].fields_by_name['name']._serialized_options = b'\372B\006r\004\020\001\030d' + _globals['_TAG'].fields_by_name['slug']._loaded_options = None + _globals['_TAG'].fields_by_name['slug']._serialized_options = b'\372B\030r\026\020\001\030d2\020^[-a-zA-Z0-9_]+$' + _globals['_TAG'].fields_by_name['color']._loaded_options = None + _globals['_TAG'].fields_by_name['color']._serialized_options = b'\372B\025r\023\020\006\030\0062\r^[0-9a-f]{6}$' + _globals['_ENTITY'].fields_by_name['timestamp']._loaded_options = None + _globals['_ENTITY'].fields_by_name['timestamp']._serialized_options = b'\372B\007\262\001\004\010\0018\001' + _globals['_INGESTREQUEST'].fields_by_name['stream']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['stream']._serialized_options = b'\372B\007r\005\020\001\030\377\001' + _globals['_INGESTREQUEST'].fields_by_name['entities']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['entities']._serialized_options = b'\372B\010\222\001\005\010\001\020\350\007' + _globals['_INGESTREQUEST'].fields_by_name['id']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['id']._serialized_options = b'\372B\005r\003\260\001\001' + _globals['_INGESTREQUEST'].fields_by_name['producer_app_name']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['producer_app_name']._serialized_options = b'\372B\007r\005\020\001\030\377\001' + _globals['_INGESTREQUEST'].fields_by_name['producer_app_version']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['producer_app_version']._serialized_options = b'\372B\007r\005\020\001\030\377\001' + _globals['_INGESTREQUEST'].fields_by_name['sdk_name']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['sdk_name']._serialized_options = b'\372B\007r\005\020\001\030\377\001' + _globals['_INGESTREQUEST'].fields_by_name['sdk_version']._loaded_options = None + _globals['_INGESTREQUEST'].fields_by_name['sdk_version']._serialized_options = b'\372B\031r\0272\025^(\\d)+\\.(\\d)+\\.(\\d)+$' + _globals['_DEVICE']._serialized_start=96 + _globals['_DEVICE']._serialized_end=741 + _globals['_INTERFACE']._serialized_start=744 + _globals['_INTERFACE']._serialized_end=2873 + _globals['_IPADDRESS']._serialized_start=2876 + _globals['_IPADDRESS']._serialized_end=3343 + _globals['_DEVICETYPE']._serialized_start=3346 + _globals['_DEVICETYPE']._serialized_end=3649 + _globals['_MANUFACTURER']._serialized_start=3652 + _globals['_MANUFACTURER']._serialized_end=3825 + _globals['_PLATFORM']._serialized_start=3828 + _globals['_PLATFORM']._serialized_end=4057 + _globals['_PREFIX']._serialized_start=4060 + _globals['_PREFIX']._serialized_end=4378 + _globals['_ROLE']._serialized_start=4381 + _globals['_ROLE']._serialized_end=4594 + _globals['_SITE']._serialized_start=4597 + _globals['_SITE']._serialized_end=4939 + _globals['_TAG']._serialized_start=4942 + _globals['_TAG']._serialized_end=5075 + _globals['_ENTITY']._serialized_start=5078 + _globals['_ENTITY']._serialized_end=5619 + _globals['_INGESTREQUEST']._serialized_start=5622 + _globals['_INGESTREQUEST']._serialized_end=5978 + _globals['_INGESTRESPONSE']._serialized_start=5980 + _globals['_INGESTRESPONSE']._serialized_end=6020 + _globals['_INGESTERSERVICE']._serialized_start=6022 + _globals['_INGESTERSERVICE']._serialized_end=6102 +# @@protoc_insertion_point(module_scope) diff --git a/netboxlabs/diode/sdk/diode/v1/ingester_pb2.pyi b/netboxlabs/diode/sdk/diode/v1/ingester_pb2.pyi new file mode 100644 index 0000000..689a1a1 --- /dev/null +++ b/netboxlabs/diode/sdk/diode/v1/ingester_pb2.pyi @@ -0,0 +1,248 @@ +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from netboxlabs.diode.sdk.validate import validate_pb2 as _validate_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class Device(_message.Message): + __slots__ = ("name", "device_fqdn", "device_type", "role", "platform", "serial", "site", "asset_tag", "status", "description", "comments", "tags", "primary_ip4", "primary_ip6") + NAME_FIELD_NUMBER: _ClassVar[int] + DEVICE_FQDN_FIELD_NUMBER: _ClassVar[int] + DEVICE_TYPE_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + PLATFORM_FIELD_NUMBER: _ClassVar[int] + SERIAL_FIELD_NUMBER: _ClassVar[int] + SITE_FIELD_NUMBER: _ClassVar[int] + ASSET_TAG_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + COMMENTS_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + PRIMARY_IP4_FIELD_NUMBER: _ClassVar[int] + PRIMARY_IP6_FIELD_NUMBER: _ClassVar[int] + name: str + device_fqdn: str + device_type: DeviceType + role: Role + platform: Platform + serial: str + site: Site + asset_tag: str + status: str + description: str + comments: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + primary_ip4: IPAddress + primary_ip6: IPAddress + def __init__(self, name: _Optional[str] = ..., device_fqdn: _Optional[str] = ..., device_type: _Optional[_Union[DeviceType, _Mapping]] = ..., role: _Optional[_Union[Role, _Mapping]] = ..., platform: _Optional[_Union[Platform, _Mapping]] = ..., serial: _Optional[str] = ..., site: _Optional[_Union[Site, _Mapping]] = ..., asset_tag: _Optional[str] = ..., status: _Optional[str] = ..., description: _Optional[str] = ..., comments: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ..., primary_ip4: _Optional[_Union[IPAddress, _Mapping]] = ..., primary_ip6: _Optional[_Union[IPAddress, _Mapping]] = ...) -> None: ... + +class Interface(_message.Message): + __slots__ = ("device", "name", "label", "type", "enabled", "mtu", "mac_address", "speed", "wwn", "mgmt_only", "description", "mark_connected", "mode", "tags") + DEVICE_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + LABEL_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + ENABLED_FIELD_NUMBER: _ClassVar[int] + MTU_FIELD_NUMBER: _ClassVar[int] + MAC_ADDRESS_FIELD_NUMBER: _ClassVar[int] + SPEED_FIELD_NUMBER: _ClassVar[int] + WWN_FIELD_NUMBER: _ClassVar[int] + MGMT_ONLY_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + MARK_CONNECTED_FIELD_NUMBER: _ClassVar[int] + MODE_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + device: Device + name: str + label: str + type: str + enabled: bool + mtu: int + mac_address: str + speed: int + wwn: str + mgmt_only: bool + description: str + mark_connected: bool + mode: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, device: _Optional[_Union[Device, _Mapping]] = ..., name: _Optional[str] = ..., label: _Optional[str] = ..., type: _Optional[str] = ..., enabled: bool = ..., mtu: _Optional[int] = ..., mac_address: _Optional[str] = ..., speed: _Optional[int] = ..., wwn: _Optional[str] = ..., mgmt_only: bool = ..., description: _Optional[str] = ..., mark_connected: bool = ..., mode: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class IPAddress(_message.Message): + __slots__ = ("address", "interface", "status", "role", "dns_name", "description", "comments", "tags") + ADDRESS_FIELD_NUMBER: _ClassVar[int] + INTERFACE_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + ROLE_FIELD_NUMBER: _ClassVar[int] + DNS_NAME_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + COMMENTS_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + address: str + interface: Interface + status: str + role: str + dns_name: str + description: str + comments: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, address: _Optional[str] = ..., interface: _Optional[_Union[Interface, _Mapping]] = ..., status: _Optional[str] = ..., role: _Optional[str] = ..., dns_name: _Optional[str] = ..., description: _Optional[str] = ..., comments: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class DeviceType(_message.Message): + __slots__ = ("model", "slug", "manufacturer", "description", "comments", "part_number", "tags") + MODEL_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + MANUFACTURER_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + COMMENTS_FIELD_NUMBER: _ClassVar[int] + PART_NUMBER_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + model: str + slug: str + manufacturer: Manufacturer + description: str + comments: str + part_number: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, model: _Optional[str] = ..., slug: _Optional[str] = ..., manufacturer: _Optional[_Union[Manufacturer, _Mapping]] = ..., description: _Optional[str] = ..., comments: _Optional[str] = ..., part_number: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Manufacturer(_message.Message): + __slots__ = ("name", "slug", "description", "tags") + NAME_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + name: str + slug: str + description: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, name: _Optional[str] = ..., slug: _Optional[str] = ..., description: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Platform(_message.Message): + __slots__ = ("name", "slug", "manufacturer", "description", "tags") + NAME_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + MANUFACTURER_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + name: str + slug: str + manufacturer: Manufacturer + description: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, name: _Optional[str] = ..., slug: _Optional[str] = ..., manufacturer: _Optional[_Union[Manufacturer, _Mapping]] = ..., description: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Prefix(_message.Message): + __slots__ = ("prefix", "site", "status", "is_pool", "mark_utilized", "description", "comments", "tags") + PREFIX_FIELD_NUMBER: _ClassVar[int] + SITE_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + IS_POOL_FIELD_NUMBER: _ClassVar[int] + MARK_UTILIZED_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + COMMENTS_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + prefix: str + site: Site + status: str + is_pool: bool + mark_utilized: bool + description: str + comments: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, prefix: _Optional[str] = ..., site: _Optional[_Union[Site, _Mapping]] = ..., status: _Optional[str] = ..., is_pool: bool = ..., mark_utilized: bool = ..., description: _Optional[str] = ..., comments: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Role(_message.Message): + __slots__ = ("name", "slug", "color", "description", "tags") + NAME_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + COLOR_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + name: str + slug: str + color: str + description: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, name: _Optional[str] = ..., slug: _Optional[str] = ..., color: _Optional[str] = ..., description: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Site(_message.Message): + __slots__ = ("name", "slug", "status", "facility", "time_zone", "description", "comments", "tags") + NAME_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + FACILITY_FIELD_NUMBER: _ClassVar[int] + TIME_ZONE_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + COMMENTS_FIELD_NUMBER: _ClassVar[int] + TAGS_FIELD_NUMBER: _ClassVar[int] + name: str + slug: str + status: str + facility: str + time_zone: str + description: str + comments: str + tags: _containers.RepeatedCompositeFieldContainer[Tag] + def __init__(self, name: _Optional[str] = ..., slug: _Optional[str] = ..., status: _Optional[str] = ..., facility: _Optional[str] = ..., time_zone: _Optional[str] = ..., description: _Optional[str] = ..., comments: _Optional[str] = ..., tags: _Optional[_Iterable[_Union[Tag, _Mapping]]] = ...) -> None: ... + +class Tag(_message.Message): + __slots__ = ("name", "slug", "color") + NAME_FIELD_NUMBER: _ClassVar[int] + SLUG_FIELD_NUMBER: _ClassVar[int] + COLOR_FIELD_NUMBER: _ClassVar[int] + name: str + slug: str + color: str + def __init__(self, name: _Optional[str] = ..., slug: _Optional[str] = ..., color: _Optional[str] = ...) -> None: ... + +class Entity(_message.Message): + __slots__ = ("site", "platform", "manufacturer", "device", "device_role", "device_type", "interface", "ip_address", "prefix", "timestamp") + SITE_FIELD_NUMBER: _ClassVar[int] + PLATFORM_FIELD_NUMBER: _ClassVar[int] + MANUFACTURER_FIELD_NUMBER: _ClassVar[int] + DEVICE_FIELD_NUMBER: _ClassVar[int] + DEVICE_ROLE_FIELD_NUMBER: _ClassVar[int] + DEVICE_TYPE_FIELD_NUMBER: _ClassVar[int] + INTERFACE_FIELD_NUMBER: _ClassVar[int] + IP_ADDRESS_FIELD_NUMBER: _ClassVar[int] + PREFIX_FIELD_NUMBER: _ClassVar[int] + TIMESTAMP_FIELD_NUMBER: _ClassVar[int] + site: Site + platform: Platform + manufacturer: Manufacturer + device: Device + device_role: Role + device_type: DeviceType + interface: Interface + ip_address: IPAddress + prefix: Prefix + timestamp: _timestamp_pb2.Timestamp + def __init__(self, site: _Optional[_Union[Site, _Mapping]] = ..., platform: _Optional[_Union[Platform, _Mapping]] = ..., manufacturer: _Optional[_Union[Manufacturer, _Mapping]] = ..., device: _Optional[_Union[Device, _Mapping]] = ..., device_role: _Optional[_Union[Role, _Mapping]] = ..., device_type: _Optional[_Union[DeviceType, _Mapping]] = ..., interface: _Optional[_Union[Interface, _Mapping]] = ..., ip_address: _Optional[_Union[IPAddress, _Mapping]] = ..., prefix: _Optional[_Union[Prefix, _Mapping]] = ..., timestamp: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class IngestRequest(_message.Message): + __slots__ = ("stream", "entities", "id", "producer_app_name", "producer_app_version", "sdk_name", "sdk_version") + STREAM_FIELD_NUMBER: _ClassVar[int] + ENTITIES_FIELD_NUMBER: _ClassVar[int] + ID_FIELD_NUMBER: _ClassVar[int] + PRODUCER_APP_NAME_FIELD_NUMBER: _ClassVar[int] + PRODUCER_APP_VERSION_FIELD_NUMBER: _ClassVar[int] + SDK_NAME_FIELD_NUMBER: _ClassVar[int] + SDK_VERSION_FIELD_NUMBER: _ClassVar[int] + stream: str + entities: _containers.RepeatedCompositeFieldContainer[Entity] + id: str + producer_app_name: str + producer_app_version: str + sdk_name: str + sdk_version: str + def __init__(self, stream: _Optional[str] = ..., entities: _Optional[_Iterable[_Union[Entity, _Mapping]]] = ..., id: _Optional[str] = ..., producer_app_name: _Optional[str] = ..., producer_app_version: _Optional[str] = ..., sdk_name: _Optional[str] = ..., sdk_version: _Optional[str] = ...) -> None: ... + +class IngestResponse(_message.Message): + __slots__ = ("errors",) + ERRORS_FIELD_NUMBER: _ClassVar[int] + errors: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, errors: _Optional[_Iterable[str]] = ...) -> None: ... diff --git a/netboxlabs/diode/sdk/diode/v1/ingester_pb2_grpc.py b/netboxlabs/diode/sdk/diode/v1/ingester_pb2_grpc.py new file mode 100644 index 0000000..8fb6144 --- /dev/null +++ b/netboxlabs/diode/sdk/diode/v1/ingester_pb2_grpc.py @@ -0,0 +1,70 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from netboxlabs.diode.sdk.diode.v1 import ingester_pb2 as diode_dot_v1_dot_ingester__pb2 + + +class IngesterServiceStub(object): + """Ingestion API + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Ingest = channel.unary_unary( + '/diode.v1.IngesterService/Ingest', + request_serializer=diode_dot_v1_dot_ingester__pb2.IngestRequest.SerializeToString, + response_deserializer=diode_dot_v1_dot_ingester__pb2.IngestResponse.FromString, + ) + + +class IngesterServiceServicer(object): + """Ingestion API + """ + + def Ingest(self, request, context): + """Ingests data into the system + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_IngesterServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Ingest': grpc.unary_unary_rpc_method_handler( + servicer.Ingest, + request_deserializer=diode_dot_v1_dot_ingester__pb2.IngestRequest.FromString, + response_serializer=diode_dot_v1_dot_ingester__pb2.IngestResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'diode.v1.IngesterService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class IngesterService(object): + """Ingestion API + """ + + @staticmethod + def Ingest(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/diode.v1.IngesterService/Ingest', + diode_dot_v1_dot_ingester__pb2.IngestRequest.SerializeToString, + diode_dot_v1_dot_ingester__pb2.IngestResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/netboxlabs/diode/sdk/exceptions.py b/netboxlabs/diode/sdk/exceptions.py new file mode 100644 index 0000000..a831f1c --- /dev/null +++ b/netboxlabs/diode/sdk/exceptions.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs, Diode - SDK - Exceptions.""" + +import grpc +from google.protobuf.json_format import MessageToDict +from grpc import RpcError +from grpc_status import rpc_status + + +class BaseError(Exception): + """Base error class for Diode SDK.""" + + pass + + +class DiodeConfigError(BaseError): + """Diode Config Error.""" + + pass + + +class DiodeClientError(RpcError): + """Diode Client Error.""" + + _status_code = None + _details = None + _grpc_status = None + + def __init__(self, err: RpcError): + """Initialize DiodeClientError.""" + self._status_code = err.code() + self._details = err.details() + + @property + def status_code(self): + """Return status code.""" + return self._status_code + + @property + def details(self): + """Return error details.""" + return self._details + + def __repr__(self): + """Return string representation.""" + return f"" diff --git a/netboxlabs/diode/sdk/ingester.py b/netboxlabs/diode/sdk/ingester.py new file mode 100644 index 0000000..53ab1d4 --- /dev/null +++ b/netboxlabs/diode/sdk/ingester.py @@ -0,0 +1,494 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs, Diode - SDK - ingester protobuf message wrappers.""" +from typing import Any + +from google.protobuf import timestamp_pb2 as _timestamp_pb2 + +# ruff: noqa: I001 +from netboxlabs.diode.sdk.diode.v1.ingester_pb2 import ( + Device as DevicePb, + DeviceType as DeviceTypePb, + Entity as EntityPb, + IPAddress as IPAddressPb, + Interface as InterfacePb, + Manufacturer as ManufacturerPb, + Platform as PlatformPb, + Prefix as PrefixPb, + Role as RolePb, + Site as SitePb, + Tag as TagPb, +) + + +def convert_to_protobuf(value: Any, protobuf_class, **kwargs): + """Convert a value to a protobuf message.""" + if isinstance(value, str): + return protobuf_class(**kwargs) + return value + + +class Tag: + """Tag message wrapper.""" + + def __new__( + cls, + name: str | None = None, + slug: str | None = None, + color: str | None = None, + ) -> TagPb: + """Create a new Tag protobuf message.""" + return TagPb(name=name, slug=slug, color=color) + + +class Manufacturer: + """Manufacturer message wrapper.""" + + def __new__( + cls, + name: str | None = None, + slug: str | None = None, + description: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> ManufacturerPb: + """Create a new Manufacturer protobuf message.""" + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return ManufacturerPb( + name=name, + slug=slug, + description=description, + tags=tags, + ) + + +class Platform: + """Platform message wrapper.""" + + def __new__( + cls, + name: str | None = None, + slug: str | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + description: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> PlatformPb: + """Create a new Platform protobuf message.""" + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return PlatformPb( + name=name, + slug=slug, + manufacturer=manufacturer, + description=description, + tags=tags, + ) + + +class Role: + """Role message wrapper.""" + + def __new__( + cls, + name: str | None = None, + slug: str | None = None, + color: str | None = None, + description: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> RolePb: + """Create a new Role protobuf message.""" + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return RolePb( + name=name, + slug=slug, + color=color, + description=description, + tags=tags, + ) + + +class DeviceType: + """DeviceType message wrapper.""" + + def __new__( + cls, + model: str | None = None, + slug: str | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + description: str | None = None, + comments: str | None = None, + part_number: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> DeviceTypePb: + """Create a new DeviceType protobuf message.""" + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return DeviceTypePb( + model=model, + slug=slug, + manufacturer=manufacturer, + description=description, + comments=comments, + part_number=part_number, + tags=tags, + ) + + +class Device: + """Device message wrapper.""" + + def __new__( + cls, + name: str | None = None, + device_type: str | DeviceType | DeviceTypePb | None = None, + device_fqdn: str | None = None, + role: str | Role | RolePb | None = None, + platform: str | Platform | PlatformPb | None = None, + serial: str | None = None, + site: str | SitePb | None = None, + asset_tag: str | None = None, + status: str | None = None, + description: str | None = None, + comments: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + primary_ip4: str | IPAddressPb | None = None, + primary_ip6: str | IPAddressPb | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + ) -> DevicePb: + """Create a new Device protobuf message.""" + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + platform = convert_to_protobuf( + platform, PlatformPb, name=platform, manufacturer=manufacturer + ) + + if ( + isinstance(platform, PlatformPb) + and not platform.HasField("manufacturer") + and manufacturer is not None + ): + platform.manufacturer.CopyFrom(manufacturer) + + site = convert_to_protobuf(site, SitePb, name=site) + device_type = convert_to_protobuf( + device_type, DeviceTypePb, model=device_type, manufacturer=manufacturer + ) + + if ( + isinstance(device_type, DeviceTypePb) + and not device_type.HasField("manufacturer") + and manufacturer is not None + ): + device_type.manufacturer.CopyFrom(manufacturer) + + role = convert_to_protobuf(role, RolePb, name=role) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + primary_ip4 = convert_to_protobuf(primary_ip4, IPAddressPb, address=primary_ip4) + primary_ip6 = convert_to_protobuf(primary_ip6, IPAddressPb, address=primary_ip6) + + return DevicePb( + name=name, + device_fqdn=device_fqdn, + device_type=device_type, + role=role, + platform=platform, + serial=serial, + site=site, + asset_tag=asset_tag, + status=status, + description=description, + comments=comments, + primary_ip4=primary_ip4, + primary_ip6=primary_ip6, + tags=tags, + ) + + +class Interface: + """Interface message wrapper.""" + + def __new__( + cls, + name: str | None = None, + device: str | Device | DevicePb | None = None, + device_type: str | DeviceType | DeviceTypePb | None = None, + role: str | Role | RolePb | None = None, + platform: str | Platform | PlatformPb | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + site: str | SitePb | None = None, + type: str | None = None, + enabled: bool | None = None, + mtu: int | None = None, + mac_address: str | None = None, + speed: int | None = None, + wwn: str | None = None, + mgmt_only: bool | None = None, + description: str | None = None, + mark_connected: bool | None = None, + mode: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> InterfacePb: + """Create a new Interface protobuf message.""" + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + + platform = convert_to_protobuf( + platform, PlatformPb, name=platform, manufacturer=manufacturer + ) + + if ( + isinstance(platform, PlatformPb) + and not platform.HasField("manufacturer") + and manufacturer is not None + ): + platform.manufacturer.CopyFrom(manufacturer) + + site = convert_to_protobuf(site, SitePb, name=site) + + device_type = convert_to_protobuf( + device_type, DeviceTypePb, model=device_type, manufacturer=manufacturer + ) + + if ( + isinstance(device_type, DeviceTypePb) + and not device_type.HasField("manufacturer") + and manufacturer is not None + ): + device_type.manufacturer.CopyFrom(manufacturer) + + role = convert_to_protobuf(role, RolePb, name=role) + + device = convert_to_protobuf( + device, + DevicePb, + name=device, + device_type=device_type, + platform=platform, + site=site, + role=role, + ) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return InterfacePb( + name=name, + device=device, + type=type, + enabled=enabled, + mtu=mtu, + mac_address=mac_address, + speed=speed, + wwn=wwn, + mgmt_only=mgmt_only, + description=description, + mark_connected=mark_connected, + mode=mode, + tags=tags, + ) + + +class IPAddress: + """IPAddress message wrapper.""" + + def __new__( + cls, + address: str | None = None, + interface: str | Interface | InterfacePb | None = None, + device: str | Device | DevicePb | None = None, + device_type: str | DeviceType | DeviceTypePb | None = None, + device_role: str | Role | RolePb | None = None, + platform: str | Platform | PlatformPb | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + site: str | SitePb | None = None, + status: str | None = None, + role: str | None = None, + dns_name: str | None = None, + description: str | None = None, + comments: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> IPAddressPb: + """Create a new IPAddress protobuf message.""" + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + + platform = convert_to_protobuf( + platform, PlatformPb, name=platform, manufacturer=manufacturer + ) + + if ( + isinstance(platform, PlatformPb) + and not platform.HasField("manufacturer") + and manufacturer is not None + ): + platform.manufacturer.CopyFrom(manufacturer) + + site = convert_to_protobuf(site, SitePb, name=site) + + device_type = convert_to_protobuf( + device_type, DeviceTypePb, model=device_type, manufacturer=manufacturer + ) + + if ( + isinstance(device_type, DeviceTypePb) + and not device_type.HasField("manufacturer") + and manufacturer is not None + ): + device_type.manufacturer.CopyFrom(manufacturer) + + device_role = convert_to_protobuf(device_role, RolePb, name=device_role) + + device = convert_to_protobuf( + device, + DevicePb, + name=device, + device_type=device_type, + platform=platform, + site=site, + role=device_role, + ) + + interface = convert_to_protobuf( + interface, + InterfacePb, + name=interface, + device=device, + ) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return IPAddressPb( + address=address, + interface=interface, + status=status, + role=role, + dns_name=dns_name, + description=description, + comments=comments, + tags=tags, + ) + + +class Prefix: + """Prefix message wrapper.""" + + def __new__( + cls, + prefix: str | None = None, + site: str | SitePb | None = None, + status: str | None = None, + is_pool: bool | None = None, + mark_utilized: bool | None = None, + description: str | None = None, + comments: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> PrefixPb: + """Create a new Prefix protobuf message.""" + site = convert_to_protobuf(site, SitePb, name=site) + + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return PrefixPb( + prefix=prefix, + site=site, + status=status, + is_pool=is_pool, + mark_utilized=mark_utilized, + description=description, + comments=comments, + tags=tags, + ) + + +class Site: + """Site message wrapper.""" + + def __new__( + cls, + name: str | None = None, + slug: str | None = None, + status: str | None = None, + facility: str | None = None, + time_zone: str | None = None, + description: str | None = None, + comments: str | None = None, + tags: list[str | Tag | TagPb] | None = None, + ) -> SitePb: + """Create a new Site protobuf message.""" + if isinstance(tags, list) and all(isinstance(t, str) for t in tags): + tags = [TagPb(name=tag) for tag in tags] + + return SitePb( + name=name, + slug=slug, + status=status, + facility=facility, + time_zone=time_zone, + description=description, + comments=comments, + tags=tags, + ) + + +class Entity: + """Entity message wrapper.""" + + def __new__( + cls, + site: str | Site | SitePb | None = None, + platform: str | Platform | PlatformPb | None = None, + manufacturer: str | Manufacturer | ManufacturerPb | None = None, + device: str | Device | DevicePb | None = None, + device_role: str | Role | RolePb | None = None, + device_type: str | DeviceType | DeviceTypePb | None = None, + interface: str | Interface | InterfacePb | None = None, + ip_address: str | IPAddress | IPAddressPb | None = None, + prefix: str | Prefix | PrefixPb | None = None, + timestamp: _timestamp_pb2.Timestamp | None = None, + ): + """Create a new Entity protobuf message.""" + site = convert_to_protobuf(site, SitePb, name=site) + platform = convert_to_protobuf(platform, PlatformPb, name=platform) + manufacturer = convert_to_protobuf( + manufacturer, ManufacturerPb, name=manufacturer + ) + device = convert_to_protobuf(device, DevicePb, name=device) + device_role = convert_to_protobuf(device_role, RolePb, name=device_role) + device_type = convert_to_protobuf(device_type, DeviceTypePb, model=device_type) + ip_address = convert_to_protobuf(ip_address, IPAddressPb, address=ip_address) + interface = convert_to_protobuf(interface, InterfacePb, name=interface) + prefix = convert_to_protobuf(prefix, PrefixPb, prefix=prefix) + + return EntityPb( + site=site, + platform=platform, + manufacturer=manufacturer, + device=device, + device_role=device_role, + device_type=device_type, + interface=interface, + ip_address=ip_address, + prefix=prefix, + timestamp=timestamp, + ) diff --git a/netboxlabs/diode/sdk/validate/__init__.py b/netboxlabs/diode/sdk/validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netboxlabs/diode/sdk/validate/validate_pb2.py b/netboxlabs/diode/sdk/validate/validate_pb2.py new file mode 100644 index 0000000..cb59def --- /dev/null +++ b/netboxlabs/diode/sdk/validate/validate_pb2.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: validate/validate.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 +from google.protobuf import duration_pb2 as google_dot_protobuf_dot_duration__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17validate/validate.proto\x12\x08validate\x1a google/protobuf/descriptor.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xc8\x08\n\nFieldRules\x12\x30\n\x07message\x18\x11 \x01(\x0b\x32\x16.validate.MessageRulesR\x07message\x12,\n\x05\x66loat\x18\x01 \x01(\x0b\x32\x14.validate.FloatRulesH\x00R\x05\x66loat\x12/\n\x06\x64ouble\x18\x02 \x01(\x0b\x32\x15.validate.DoubleRulesH\x00R\x06\x64ouble\x12,\n\x05int32\x18\x03 \x01(\x0b\x32\x14.validate.Int32RulesH\x00R\x05int32\x12,\n\x05int64\x18\x04 \x01(\x0b\x32\x14.validate.Int64RulesH\x00R\x05int64\x12/\n\x06uint32\x18\x05 \x01(\x0b\x32\x15.validate.UInt32RulesH\x00R\x06uint32\x12/\n\x06uint64\x18\x06 \x01(\x0b\x32\x15.validate.UInt64RulesH\x00R\x06uint64\x12/\n\x06sint32\x18\x07 \x01(\x0b\x32\x15.validate.SInt32RulesH\x00R\x06sint32\x12/\n\x06sint64\x18\x08 \x01(\x0b\x32\x15.validate.SInt64RulesH\x00R\x06sint64\x12\x32\n\x07\x66ixed32\x18\t \x01(\x0b\x32\x16.validate.Fixed32RulesH\x00R\x07\x66ixed32\x12\x32\n\x07\x66ixed64\x18\n \x01(\x0b\x32\x16.validate.Fixed64RulesH\x00R\x07\x66ixed64\x12\x35\n\x08sfixed32\x18\x0b \x01(\x0b\x32\x17.validate.SFixed32RulesH\x00R\x08sfixed32\x12\x35\n\x08sfixed64\x18\x0c \x01(\x0b\x32\x17.validate.SFixed64RulesH\x00R\x08sfixed64\x12)\n\x04\x62ool\x18\r \x01(\x0b\x32\x13.validate.BoolRulesH\x00R\x04\x62ool\x12/\n\x06string\x18\x0e \x01(\x0b\x32\x15.validate.StringRulesH\x00R\x06string\x12,\n\x05\x62ytes\x18\x0f \x01(\x0b\x32\x14.validate.BytesRulesH\x00R\x05\x62ytes\x12)\n\x04\x65num\x18\x10 \x01(\x0b\x32\x13.validate.EnumRulesH\x00R\x04\x65num\x12\x35\n\x08repeated\x18\x12 \x01(\x0b\x32\x17.validate.RepeatedRulesH\x00R\x08repeated\x12&\n\x03map\x18\x13 \x01(\x0b\x32\x12.validate.MapRulesH\x00R\x03map\x12&\n\x03\x61ny\x18\x14 \x01(\x0b\x32\x12.validate.AnyRulesH\x00R\x03\x61ny\x12\x35\n\x08\x64uration\x18\x15 \x01(\x0b\x32\x17.validate.DurationRulesH\x00R\x08\x64uration\x12\x38\n\ttimestamp\x18\x16 \x01(\x0b\x32\x18.validate.TimestampRulesH\x00R\ttimestampB\x06\n\x04type\"\xb0\x01\n\nFloatRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x02R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x02R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x02R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x02R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x02R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x02R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x02R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb1\x01\n\x0b\x44oubleRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x01R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x01R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x01R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x01R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x01R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x01R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x01R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb0\x01\n\nInt32Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x05R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x05R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x05R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x05R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x05R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x05R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x05R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb0\x01\n\nInt64Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x03R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x03R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x03R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x03R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x03R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x03R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x03R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb1\x01\n\x0bUInt32Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\rR\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\rR\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\rR\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\rR\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\rR\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\rR\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\rR\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb1\x01\n\x0bUInt64Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x04R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x04R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x04R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x04R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x04R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x04R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x04R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb1\x01\n\x0bSInt32Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x11R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x11R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x11R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x11R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x11R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x11R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x11R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb1\x01\n\x0bSInt64Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x12R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x12R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x12R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x12R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x12R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x12R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x12R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb2\x01\n\x0c\x46ixed32Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x07R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x07R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x07R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x07R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x07R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x07R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x07R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb2\x01\n\x0c\x46ixed64Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x06R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x06R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x06R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x06R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x06R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x06R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x06R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb3\x01\n\rSFixed32Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x0fR\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x0fR\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x0fR\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x0fR\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x0fR\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x0fR\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x0fR\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"\xb3\x01\n\rSFixed64Rules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x10R\x05\x63onst\x12\x0e\n\x02lt\x18\x02 \x01(\x10R\x02lt\x12\x10\n\x03lte\x18\x03 \x01(\x10R\x03lte\x12\x0e\n\x02gt\x18\x04 \x01(\x10R\x02gt\x12\x10\n\x03gte\x18\x05 \x01(\x10R\x03gte\x12\x0e\n\x02in\x18\x06 \x03(\x10R\x02in\x12\x15\n\x06not_in\x18\x07 \x03(\x10R\x05notIn\x12!\n\x0cignore_empty\x18\x08 \x01(\x08R\x0bignoreEmpty\"!\n\tBoolRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x08R\x05\x63onst\"\xd4\x05\n\x0bStringRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\tR\x05\x63onst\x12\x10\n\x03len\x18\x13 \x01(\x04R\x03len\x12\x17\n\x07min_len\x18\x02 \x01(\x04R\x06minLen\x12\x17\n\x07max_len\x18\x03 \x01(\x04R\x06maxLen\x12\x1b\n\tlen_bytes\x18\x14 \x01(\x04R\x08lenBytes\x12\x1b\n\tmin_bytes\x18\x04 \x01(\x04R\x08minBytes\x12\x1b\n\tmax_bytes\x18\x05 \x01(\x04R\x08maxBytes\x12\x18\n\x07pattern\x18\x06 \x01(\tR\x07pattern\x12\x16\n\x06prefix\x18\x07 \x01(\tR\x06prefix\x12\x16\n\x06suffix\x18\x08 \x01(\tR\x06suffix\x12\x1a\n\x08\x63ontains\x18\t \x01(\tR\x08\x63ontains\x12!\n\x0cnot_contains\x18\x17 \x01(\tR\x0bnotContains\x12\x0e\n\x02in\x18\n \x03(\tR\x02in\x12\x15\n\x06not_in\x18\x0b \x03(\tR\x05notIn\x12\x16\n\x05\x65mail\x18\x0c \x01(\x08H\x00R\x05\x65mail\x12\x1c\n\x08hostname\x18\r \x01(\x08H\x00R\x08hostname\x12\x10\n\x02ip\x18\x0e \x01(\x08H\x00R\x02ip\x12\x14\n\x04ipv4\x18\x0f \x01(\x08H\x00R\x04ipv4\x12\x14\n\x04ipv6\x18\x10 \x01(\x08H\x00R\x04ipv6\x12\x12\n\x03uri\x18\x11 \x01(\x08H\x00R\x03uri\x12\x19\n\x07uri_ref\x18\x12 \x01(\x08H\x00R\x06uriRef\x12\x1a\n\x07\x61\x64\x64ress\x18\x15 \x01(\x08H\x00R\x07\x61\x64\x64ress\x12\x14\n\x04uuid\x18\x16 \x01(\x08H\x00R\x04uuid\x12@\n\x10well_known_regex\x18\x18 \x01(\x0e\x32\x14.validate.KnownRegexH\x00R\x0ewellKnownRegex\x12\x1c\n\x06strict\x18\x19 \x01(\x08:\x04trueR\x06strict\x12!\n\x0cignore_empty\x18\x1a \x01(\x08R\x0bignoreEmptyB\x0c\n\nwell_known\"\xe2\x02\n\nBytesRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x0cR\x05\x63onst\x12\x10\n\x03len\x18\r \x01(\x04R\x03len\x12\x17\n\x07min_len\x18\x02 \x01(\x04R\x06minLen\x12\x17\n\x07max_len\x18\x03 \x01(\x04R\x06maxLen\x12\x18\n\x07pattern\x18\x04 \x01(\tR\x07pattern\x12\x16\n\x06prefix\x18\x05 \x01(\x0cR\x06prefix\x12\x16\n\x06suffix\x18\x06 \x01(\x0cR\x06suffix\x12\x1a\n\x08\x63ontains\x18\x07 \x01(\x0cR\x08\x63ontains\x12\x0e\n\x02in\x18\x08 \x03(\x0cR\x02in\x12\x15\n\x06not_in\x18\t \x03(\x0cR\x05notIn\x12\x10\n\x02ip\x18\n \x01(\x08H\x00R\x02ip\x12\x14\n\x04ipv4\x18\x0b \x01(\x08H\x00R\x04ipv4\x12\x14\n\x04ipv6\x18\x0c \x01(\x08H\x00R\x04ipv6\x12!\n\x0cignore_empty\x18\x0e \x01(\x08R\x0bignoreEmptyB\x0c\n\nwell_known\"k\n\tEnumRules\x12\x14\n\x05\x63onst\x18\x01 \x01(\x05R\x05\x63onst\x12!\n\x0c\x64\x65\x66ined_only\x18\x02 \x01(\x08R\x0b\x64\x65\x66inedOnly\x12\x0e\n\x02in\x18\x03 \x03(\x05R\x02in\x12\x15\n\x06not_in\x18\x04 \x03(\x05R\x05notIn\">\n\x0cMessageRules\x12\x12\n\x04skip\x18\x01 \x01(\x08R\x04skip\x12\x1a\n\x08required\x18\x02 \x01(\x08R\x08required\"\xb0\x01\n\rRepeatedRules\x12\x1b\n\tmin_items\x18\x01 \x01(\x04R\x08minItems\x12\x1b\n\tmax_items\x18\x02 \x01(\x04R\x08maxItems\x12\x16\n\x06unique\x18\x03 \x01(\x08R\x06unique\x12*\n\x05items\x18\x04 \x01(\x0b\x32\x14.validate.FieldRulesR\x05items\x12!\n\x0cignore_empty\x18\x05 \x01(\x08R\x0bignoreEmpty\"\xdc\x01\n\x08MapRules\x12\x1b\n\tmin_pairs\x18\x01 \x01(\x04R\x08minPairs\x12\x1b\n\tmax_pairs\x18\x02 \x01(\x04R\x08maxPairs\x12\x1b\n\tno_sparse\x18\x03 \x01(\x08R\x08noSparse\x12(\n\x04keys\x18\x04 \x01(\x0b\x32\x14.validate.FieldRulesR\x04keys\x12,\n\x06values\x18\x05 \x01(\x0b\x32\x14.validate.FieldRulesR\x06values\x12!\n\x0cignore_empty\x18\x06 \x01(\x08R\x0bignoreEmpty\"M\n\x08\x41nyRules\x12\x1a\n\x08required\x18\x01 \x01(\x08R\x08required\x12\x0e\n\x02in\x18\x02 \x03(\tR\x02in\x12\x15\n\x06not_in\x18\x03 \x03(\tR\x05notIn\"\xe9\x02\n\rDurationRules\x12\x1a\n\x08required\x18\x01 \x01(\x08R\x08required\x12/\n\x05\x63onst\x18\x02 \x01(\x0b\x32\x19.google.protobuf.DurationR\x05\x63onst\x12)\n\x02lt\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationR\x02lt\x12+\n\x03lte\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationR\x03lte\x12)\n\x02gt\x18\x05 \x01(\x0b\x32\x19.google.protobuf.DurationR\x02gt\x12+\n\x03gte\x18\x06 \x01(\x0b\x32\x19.google.protobuf.DurationR\x03gte\x12)\n\x02in\x18\x07 \x03(\x0b\x32\x19.google.protobuf.DurationR\x02in\x12\x30\n\x06not_in\x18\x08 \x03(\x0b\x32\x19.google.protobuf.DurationR\x05notIn\"\xf3\x02\n\x0eTimestampRules\x12\x1a\n\x08required\x18\x01 \x01(\x08R\x08required\x12\x30\n\x05\x63onst\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x05\x63onst\x12*\n\x02lt\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x02lt\x12,\n\x03lte\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x03lte\x12*\n\x02gt\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x02gt\x12,\n\x03gte\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x03gte\x12\x15\n\x06lt_now\x18\x07 \x01(\x08R\x05ltNow\x12\x15\n\x06gt_now\x18\x08 \x01(\x08R\x05gtNow\x12\x31\n\x06within\x18\t \x01(\x0b\x32\x19.google.protobuf.DurationR\x06within*F\n\nKnownRegex\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x14\n\x10HTTP_HEADER_NAME\x10\x01\x12\x15\n\x11HTTP_HEADER_VALUE\x10\x02:<\n\x08\x64isabled\x12\x1f.google.protobuf.MessageOptions\x18\xaf\x08 \x01(\x08R\x08\x64isabled::\n\x07ignored\x12\x1f.google.protobuf.MessageOptions\x18\xb0\x08 \x01(\x08R\x07ignored::\n\x08required\x12\x1d.google.protobuf.OneofOptions\x18\xaf\x08 \x01(\x08R\x08required:J\n\x05rules\x12\x1d.google.protobuf.FieldOptions\x18\xaf\x08 \x01(\x0b\x32\x14.validate.FieldRulesR\x05rulesBP\n\x1aio.envoyproxy.pgv.validateZ2github.com/envoyproxy/protoc-gen-validate/validate') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'validate.validate_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\032io.envoyproxy.pgv.validateZ2github.com/envoyproxy/protoc-gen-validate/validate' + _globals['_KNOWNREGEX']._serialized_start=5909 + _globals['_KNOWNREGEX']._serialized_end=5979 + _globals['_FIELDRULES']._serialized_start=137 + _globals['_FIELDRULES']._serialized_end=1233 + _globals['_FLOATRULES']._serialized_start=1236 + _globals['_FLOATRULES']._serialized_end=1412 + _globals['_DOUBLERULES']._serialized_start=1415 + _globals['_DOUBLERULES']._serialized_end=1592 + _globals['_INT32RULES']._serialized_start=1595 + _globals['_INT32RULES']._serialized_end=1771 + _globals['_INT64RULES']._serialized_start=1774 + _globals['_INT64RULES']._serialized_end=1950 + _globals['_UINT32RULES']._serialized_start=1953 + _globals['_UINT32RULES']._serialized_end=2130 + _globals['_UINT64RULES']._serialized_start=2133 + _globals['_UINT64RULES']._serialized_end=2310 + _globals['_SINT32RULES']._serialized_start=2313 + _globals['_SINT32RULES']._serialized_end=2490 + _globals['_SINT64RULES']._serialized_start=2493 + _globals['_SINT64RULES']._serialized_end=2670 + _globals['_FIXED32RULES']._serialized_start=2673 + _globals['_FIXED32RULES']._serialized_end=2851 + _globals['_FIXED64RULES']._serialized_start=2854 + _globals['_FIXED64RULES']._serialized_end=3032 + _globals['_SFIXED32RULES']._serialized_start=3035 + _globals['_SFIXED32RULES']._serialized_end=3214 + _globals['_SFIXED64RULES']._serialized_start=3217 + _globals['_SFIXED64RULES']._serialized_end=3396 + _globals['_BOOLRULES']._serialized_start=3398 + _globals['_BOOLRULES']._serialized_end=3431 + _globals['_STRINGRULES']._serialized_start=3434 + _globals['_STRINGRULES']._serialized_end=4158 + _globals['_BYTESRULES']._serialized_start=4161 + _globals['_BYTESRULES']._serialized_end=4515 + _globals['_ENUMRULES']._serialized_start=4517 + _globals['_ENUMRULES']._serialized_end=4624 + _globals['_MESSAGERULES']._serialized_start=4626 + _globals['_MESSAGERULES']._serialized_end=4688 + _globals['_REPEATEDRULES']._serialized_start=4691 + _globals['_REPEATEDRULES']._serialized_end=4867 + _globals['_MAPRULES']._serialized_start=4870 + _globals['_MAPRULES']._serialized_end=5090 + _globals['_ANYRULES']._serialized_start=5092 + _globals['_ANYRULES']._serialized_end=5169 + _globals['_DURATIONRULES']._serialized_start=5172 + _globals['_DURATIONRULES']._serialized_end=5533 + _globals['_TIMESTAMPRULES']._serialized_start=5536 + _globals['_TIMESTAMPRULES']._serialized_end=5907 +# @@protoc_insertion_point(module_scope) diff --git a/netboxlabs/diode/sdk/validate/validate_pb2.pyi b/netboxlabs/diode/sdk/validate/validate_pb2.pyi new file mode 100644 index 0000000..2286693 --- /dev/null +++ b/netboxlabs/diode/sdk/validate/validate_pb2.pyi @@ -0,0 +1,494 @@ +from google.protobuf import descriptor_pb2 as _descriptor_pb2 +from google.protobuf import duration_pb2 as _duration_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class KnownRegex(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN: _ClassVar[KnownRegex] + HTTP_HEADER_NAME: _ClassVar[KnownRegex] + HTTP_HEADER_VALUE: _ClassVar[KnownRegex] +UNKNOWN: KnownRegex +HTTP_HEADER_NAME: KnownRegex +HTTP_HEADER_VALUE: KnownRegex +DISABLED_FIELD_NUMBER: _ClassVar[int] +disabled: _descriptor.FieldDescriptor +IGNORED_FIELD_NUMBER: _ClassVar[int] +ignored: _descriptor.FieldDescriptor +REQUIRED_FIELD_NUMBER: _ClassVar[int] +required: _descriptor.FieldDescriptor +RULES_FIELD_NUMBER: _ClassVar[int] +rules: _descriptor.FieldDescriptor + +class FieldRules(_message.Message): + __slots__ = ("message", "float", "double", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64", "bool", "string", "bytes", "enum", "repeated", "map", "any", "duration", "timestamp") + MESSAGE_FIELD_NUMBER: _ClassVar[int] + FLOAT_FIELD_NUMBER: _ClassVar[int] + DOUBLE_FIELD_NUMBER: _ClassVar[int] + INT32_FIELD_NUMBER: _ClassVar[int] + INT64_FIELD_NUMBER: _ClassVar[int] + UINT32_FIELD_NUMBER: _ClassVar[int] + UINT64_FIELD_NUMBER: _ClassVar[int] + SINT32_FIELD_NUMBER: _ClassVar[int] + SINT64_FIELD_NUMBER: _ClassVar[int] + FIXED32_FIELD_NUMBER: _ClassVar[int] + FIXED64_FIELD_NUMBER: _ClassVar[int] + SFIXED32_FIELD_NUMBER: _ClassVar[int] + SFIXED64_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_NUMBER: _ClassVar[int] + STRING_FIELD_NUMBER: _ClassVar[int] + BYTES_FIELD_NUMBER: _ClassVar[int] + ENUM_FIELD_NUMBER: _ClassVar[int] + REPEATED_FIELD_NUMBER: _ClassVar[int] + MAP_FIELD_NUMBER: _ClassVar[int] + ANY_FIELD_NUMBER: _ClassVar[int] + DURATION_FIELD_NUMBER: _ClassVar[int] + TIMESTAMP_FIELD_NUMBER: _ClassVar[int] + message: MessageRules + float: FloatRules + double: DoubleRules + int32: Int32Rules + int64: Int64Rules + uint32: UInt32Rules + uint64: UInt64Rules + sint32: SInt32Rules + sint64: SInt64Rules + fixed32: Fixed32Rules + fixed64: Fixed64Rules + sfixed32: SFixed32Rules + sfixed64: SFixed64Rules + bool: BoolRules + string: StringRules + bytes: BytesRules + enum: EnumRules + repeated: RepeatedRules + map: MapRules + any: AnyRules + duration: DurationRules + timestamp: TimestampRules + def __init__(self, message: _Optional[_Union[MessageRules, _Mapping]] = ..., float: _Optional[_Union[FloatRules, _Mapping]] = ..., double: _Optional[_Union[DoubleRules, _Mapping]] = ..., int32: _Optional[_Union[Int32Rules, _Mapping]] = ..., int64: _Optional[_Union[Int64Rules, _Mapping]] = ..., uint32: _Optional[_Union[UInt32Rules, _Mapping]] = ..., uint64: _Optional[_Union[UInt64Rules, _Mapping]] = ..., sint32: _Optional[_Union[SInt32Rules, _Mapping]] = ..., sint64: _Optional[_Union[SInt64Rules, _Mapping]] = ..., fixed32: _Optional[_Union[Fixed32Rules, _Mapping]] = ..., fixed64: _Optional[_Union[Fixed64Rules, _Mapping]] = ..., sfixed32: _Optional[_Union[SFixed32Rules, _Mapping]] = ..., sfixed64: _Optional[_Union[SFixed64Rules, _Mapping]] = ..., bool: _Optional[_Union[BoolRules, _Mapping]] = ..., string: _Optional[_Union[StringRules, _Mapping]] = ..., bytes: _Optional[_Union[BytesRules, _Mapping]] = ..., enum: _Optional[_Union[EnumRules, _Mapping]] = ..., repeated: _Optional[_Union[RepeatedRules, _Mapping]] = ..., map: _Optional[_Union[MapRules, _Mapping]] = ..., any: _Optional[_Union[AnyRules, _Mapping]] = ..., duration: _Optional[_Union[DurationRules, _Mapping]] = ..., timestamp: _Optional[_Union[TimestampRules, _Mapping]] = ...) -> None: ... + +class FloatRules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: float + lt: float + lte: float + gt: float + gte: float + not_in: _containers.RepeatedScalarFieldContainer[float] + ignore_empty: bool + def __init__(self, const: _Optional[float] = ..., lt: _Optional[float] = ..., lte: _Optional[float] = ..., gt: _Optional[float] = ..., gte: _Optional[float] = ..., not_in: _Optional[_Iterable[float]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class DoubleRules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: float + lt: float + lte: float + gt: float + gte: float + not_in: _containers.RepeatedScalarFieldContainer[float] + ignore_empty: bool + def __init__(self, const: _Optional[float] = ..., lt: _Optional[float] = ..., lte: _Optional[float] = ..., gt: _Optional[float] = ..., gte: _Optional[float] = ..., not_in: _Optional[_Iterable[float]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class Int32Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class Int64Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class UInt32Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class UInt64Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class SInt32Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class SInt64Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class Fixed32Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class Fixed64Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class SFixed32Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class SFixed64Rules(_message.Message): + __slots__ = ("const", "lt", "lte", "gt", "gte", "not_in", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: int + lt: int + lte: int + gt: int + gte: int + not_in: _containers.RepeatedScalarFieldContainer[int] + ignore_empty: bool + def __init__(self, const: _Optional[int] = ..., lt: _Optional[int] = ..., lte: _Optional[int] = ..., gt: _Optional[int] = ..., gte: _Optional[int] = ..., not_in: _Optional[_Iterable[int]] = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class BoolRules(_message.Message): + __slots__ = ("const",) + CONST_FIELD_NUMBER: _ClassVar[int] + const: bool + def __init__(self, const: bool = ...) -> None: ... + +class StringRules(_message.Message): + __slots__ = ("const", "len", "min_len", "max_len", "len_bytes", "min_bytes", "max_bytes", "pattern", "prefix", "suffix", "contains", "not_contains", "not_in", "email", "hostname", "ip", "ipv4", "ipv6", "uri", "uri_ref", "address", "uuid", "well_known_regex", "strict", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LEN_FIELD_NUMBER: _ClassVar[int] + MIN_LEN_FIELD_NUMBER: _ClassVar[int] + MAX_LEN_FIELD_NUMBER: _ClassVar[int] + LEN_BYTES_FIELD_NUMBER: _ClassVar[int] + MIN_BYTES_FIELD_NUMBER: _ClassVar[int] + MAX_BYTES_FIELD_NUMBER: _ClassVar[int] + PATTERN_FIELD_NUMBER: _ClassVar[int] + PREFIX_FIELD_NUMBER: _ClassVar[int] + SUFFIX_FIELD_NUMBER: _ClassVar[int] + CONTAINS_FIELD_NUMBER: _ClassVar[int] + NOT_CONTAINS_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + EMAIL_FIELD_NUMBER: _ClassVar[int] + HOSTNAME_FIELD_NUMBER: _ClassVar[int] + IP_FIELD_NUMBER: _ClassVar[int] + IPV4_FIELD_NUMBER: _ClassVar[int] + IPV6_FIELD_NUMBER: _ClassVar[int] + URI_FIELD_NUMBER: _ClassVar[int] + URI_REF_FIELD_NUMBER: _ClassVar[int] + ADDRESS_FIELD_NUMBER: _ClassVar[int] + UUID_FIELD_NUMBER: _ClassVar[int] + WELL_KNOWN_REGEX_FIELD_NUMBER: _ClassVar[int] + STRICT_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: str + len: int + min_len: int + max_len: int + len_bytes: int + min_bytes: int + max_bytes: int + pattern: str + prefix: str + suffix: str + contains: str + not_contains: str + not_in: _containers.RepeatedScalarFieldContainer[str] + email: bool + hostname: bool + ip: bool + ipv4: bool + ipv6: bool + uri: bool + uri_ref: bool + address: bool + uuid: bool + well_known_regex: KnownRegex + strict: bool + ignore_empty: bool + def __init__(self, const: _Optional[str] = ..., len: _Optional[int] = ..., min_len: _Optional[int] = ..., max_len: _Optional[int] = ..., len_bytes: _Optional[int] = ..., min_bytes: _Optional[int] = ..., max_bytes: _Optional[int] = ..., pattern: _Optional[str] = ..., prefix: _Optional[str] = ..., suffix: _Optional[str] = ..., contains: _Optional[str] = ..., not_contains: _Optional[str] = ..., not_in: _Optional[_Iterable[str]] = ..., email: bool = ..., hostname: bool = ..., ip: bool = ..., ipv4: bool = ..., ipv6: bool = ..., uri: bool = ..., uri_ref: bool = ..., address: bool = ..., uuid: bool = ..., well_known_regex: _Optional[_Union[KnownRegex, str]] = ..., strict: bool = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class BytesRules(_message.Message): + __slots__ = ("const", "len", "min_len", "max_len", "pattern", "prefix", "suffix", "contains", "not_in", "ip", "ipv4", "ipv6", "ignore_empty") + CONST_FIELD_NUMBER: _ClassVar[int] + LEN_FIELD_NUMBER: _ClassVar[int] + MIN_LEN_FIELD_NUMBER: _ClassVar[int] + MAX_LEN_FIELD_NUMBER: _ClassVar[int] + PATTERN_FIELD_NUMBER: _ClassVar[int] + PREFIX_FIELD_NUMBER: _ClassVar[int] + SUFFIX_FIELD_NUMBER: _ClassVar[int] + CONTAINS_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + IP_FIELD_NUMBER: _ClassVar[int] + IPV4_FIELD_NUMBER: _ClassVar[int] + IPV6_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + const: bytes + len: int + min_len: int + max_len: int + pattern: str + prefix: bytes + suffix: bytes + contains: bytes + not_in: _containers.RepeatedScalarFieldContainer[bytes] + ip: bool + ipv4: bool + ipv6: bool + ignore_empty: bool + def __init__(self, const: _Optional[bytes] = ..., len: _Optional[int] = ..., min_len: _Optional[int] = ..., max_len: _Optional[int] = ..., pattern: _Optional[str] = ..., prefix: _Optional[bytes] = ..., suffix: _Optional[bytes] = ..., contains: _Optional[bytes] = ..., not_in: _Optional[_Iterable[bytes]] = ..., ip: bool = ..., ipv4: bool = ..., ipv6: bool = ..., ignore_empty: bool = ..., **kwargs) -> None: ... + +class EnumRules(_message.Message): + __slots__ = ("const", "defined_only", "not_in") + CONST_FIELD_NUMBER: _ClassVar[int] + DEFINED_ONLY_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + const: int + defined_only: bool + not_in: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, const: _Optional[int] = ..., defined_only: bool = ..., not_in: _Optional[_Iterable[int]] = ..., **kwargs) -> None: ... + +class MessageRules(_message.Message): + __slots__ = ("skip", "required") + SKIP_FIELD_NUMBER: _ClassVar[int] + REQUIRED_FIELD_NUMBER: _ClassVar[int] + skip: bool + required: bool + def __init__(self, skip: bool = ..., required: bool = ...) -> None: ... + +class RepeatedRules(_message.Message): + __slots__ = ("min_items", "max_items", "unique", "items", "ignore_empty") + MIN_ITEMS_FIELD_NUMBER: _ClassVar[int] + MAX_ITEMS_FIELD_NUMBER: _ClassVar[int] + UNIQUE_FIELD_NUMBER: _ClassVar[int] + ITEMS_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + min_items: int + max_items: int + unique: bool + items: FieldRules + ignore_empty: bool + def __init__(self, min_items: _Optional[int] = ..., max_items: _Optional[int] = ..., unique: bool = ..., items: _Optional[_Union[FieldRules, _Mapping]] = ..., ignore_empty: bool = ...) -> None: ... + +class MapRules(_message.Message): + __slots__ = ("min_pairs", "max_pairs", "no_sparse", "keys", "values", "ignore_empty") + MIN_PAIRS_FIELD_NUMBER: _ClassVar[int] + MAX_PAIRS_FIELD_NUMBER: _ClassVar[int] + NO_SPARSE_FIELD_NUMBER: _ClassVar[int] + KEYS_FIELD_NUMBER: _ClassVar[int] + VALUES_FIELD_NUMBER: _ClassVar[int] + IGNORE_EMPTY_FIELD_NUMBER: _ClassVar[int] + min_pairs: int + max_pairs: int + no_sparse: bool + keys: FieldRules + values: FieldRules + ignore_empty: bool + def __init__(self, min_pairs: _Optional[int] = ..., max_pairs: _Optional[int] = ..., no_sparse: bool = ..., keys: _Optional[_Union[FieldRules, _Mapping]] = ..., values: _Optional[_Union[FieldRules, _Mapping]] = ..., ignore_empty: bool = ...) -> None: ... + +class AnyRules(_message.Message): + __slots__ = ("required", "not_in") + REQUIRED_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + required: bool + not_in: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, required: bool = ..., not_in: _Optional[_Iterable[str]] = ..., **kwargs) -> None: ... + +class DurationRules(_message.Message): + __slots__ = ("required", "const", "lt", "lte", "gt", "gte", "not_in") + REQUIRED_FIELD_NUMBER: _ClassVar[int] + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + IN_FIELD_NUMBER: _ClassVar[int] + NOT_IN_FIELD_NUMBER: _ClassVar[int] + required: bool + const: _duration_pb2.Duration + lt: _duration_pb2.Duration + lte: _duration_pb2.Duration + gt: _duration_pb2.Duration + gte: _duration_pb2.Duration + not_in: _containers.RepeatedCompositeFieldContainer[_duration_pb2.Duration] + def __init__(self, required: bool = ..., const: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., lt: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., lte: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., gt: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., gte: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ..., not_in: _Optional[_Iterable[_Union[_duration_pb2.Duration, _Mapping]]] = ..., **kwargs) -> None: ... + +class TimestampRules(_message.Message): + __slots__ = ("required", "const", "lt", "lte", "gt", "gte", "lt_now", "gt_now", "within") + REQUIRED_FIELD_NUMBER: _ClassVar[int] + CONST_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + LTE_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + GTE_FIELD_NUMBER: _ClassVar[int] + LT_NOW_FIELD_NUMBER: _ClassVar[int] + GT_NOW_FIELD_NUMBER: _ClassVar[int] + WITHIN_FIELD_NUMBER: _ClassVar[int] + required: bool + const: _timestamp_pb2.Timestamp + lt: _timestamp_pb2.Timestamp + lte: _timestamp_pb2.Timestamp + gt: _timestamp_pb2.Timestamp + gte: _timestamp_pb2.Timestamp + lt_now: bool + gt_now: bool + within: _duration_pb2.Duration + def __init__(self, required: bool = ..., const: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., lt: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., lte: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., gt: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., gte: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ..., lt_now: bool = ..., gt_now: bool = ..., within: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ...) -> None: ... diff --git a/netboxlabs/diode/sdk/validate/validate_pb2_grpc.py b/netboxlabs/diode/sdk/validate/validate_pb2_grpc.py new file mode 100644 index 0000000..2daafff --- /dev/null +++ b/netboxlabs/diode/sdk/validate/validate_pb2_grpc.py @@ -0,0 +1,4 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + diff --git a/netboxlabs/diode/sdk/version.py b/netboxlabs/diode/sdk/version.py new file mode 100644 index 0000000..2de2087 --- /dev/null +++ b/netboxlabs/diode/sdk/version.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""Version stamp.""" + +# These properties are injected at build time by the build process. + +__commit_hash__ = "unknown" +__track__ = "dev" +__version__ = "0.0.0" + + +def version_display(): + """Display the version, track and hash together.""" + return f"v{__version__}-{__track__}-{__commit_hash__}" + + +def version_semver(): + """Semantic version.""" + return __version__ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8099d92 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[project] +name = "netboxlabs-diode-sdk" +version = "0.0.1" # Overwritten during the build process +description = "NetBox Labs, Diode SDK" +readme = "README.md" # Optional +requires-python = ">=3.10" +license = {file = "LICENSE.txt"} +authors = [ + {name = "NetBox Labs", email = "support@netboxlabs.com" } # Optional +] +maintainers = [ + {name = "NetBox Labs", email = "support@netboxlabs.com" } # Optional +] + +classifiers = [ # Optional + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', +] + +dependencies = [ + "certifi==2024.2.2", + "grpcio==1.62.1", + "grpcio-status==1.62.1", + "sentry-sdk>=2.2.1", +] + +[project.optional-dependencies] # Optional +dev = ["black", "check-manifest", "ruff"] +test = ["coverage", "pytest", "pytest-cov"] + +[tool.coverage.run] +omit = [ + "*/netboxlabs/diode/sdk/diode/*", + "*/netboxlabs/diode/sdk/validate/*", + "*/tests/*", +] + +[project.urls] # Optional +"Homepage" = "https://netboxlabs.com/" + +[project.scripts] # Optional + +[tool.setuptools] +packages = [ + "netboxlabs.diode.sdk", + "netboxlabs.diode.sdk.diode", + "netboxlabs.diode.sdk.diode.v1", + "netboxlabs.diode.sdk.validate", +] + +[build-system] +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" + + +[tool.ruff] +line-length = 140 +exclude = [ + "netboxlabs/diode/sdk/diode/*", + "netboxlabs/diode/sdk/validate/*", +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.ruff.lint] +select = ["C", "D", "E", "F", "I", "R", "UP", "W"] +ignore = ["F401", "D203", "D212", "D400", "D401", "D404", "RET504"] diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..cbf0f52 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs - Tests.""" +import os +from unittest import mock + +import grpc +import pytest + +from netboxlabs.diode.sdk.client import ( + _DIODE_API_KEY_ENVVAR_NAME, + _DIODE_SENTRY_DSN_ENVVAR_NAME, + DiodeClient, + DiodeMethodClientInterceptor, + _ClientCallDetails, + _get_api_key, + _get_sentry_dsn, + _load_certs, + parse_target, +) +from netboxlabs.diode.sdk.exceptions import DiodeClientError, DiodeConfigError + + +def test_init(): + """Check we can initiate a client configuration.""" + config = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + assert config.target == "localhost:8081" + assert config.name == "diode-sdk-python" + assert config.version == "0.0.1" + assert config.app_name == "my-producer" + assert config.app_version == "0.0.1" + assert config.tls_verify is False + assert config.path == "" + + +def test_config_error(): + """Check we can raise a config error.""" + with pytest.raises(DiodeConfigError) as err: + DiodeClient( + target="grpc://localhost:8081", app_name="my-producer", app_version="0.0.1" + ) + assert ( + str(err.value) == "api_key param or DIODE_API_KEY environment variable required" + ) + + +def test_client_error(): + """Check we can raise a client error.""" + with pytest.raises(DiodeClientError) as err: + client = DiodeClient( + target="grpc://invalid:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + client.ingest(entities=[]) + assert err.value.status_code == grpc.StatusCode.UNAVAILABLE + assert "DNS resolution failed for invalid:8081" in err.value.details + + +def test_diode_client_error_repr_returns_correct_string(): + """Check we can return the correct string representation of the error.""" + grpc_error = grpc.RpcError() + grpc_error.code = lambda: grpc.StatusCode.UNAVAILABLE + grpc_error.details = lambda: "Some details about the error" + error = DiodeClientError(grpc_error) + error._status_code = grpc.StatusCode.UNAVAILABLE + error._details = "Some details about the error" + assert ( + repr(error) + == "" + ) + + +def test_load_certs_returns_bytes(): + """Check that _load_certs returns bytes.""" + assert isinstance(_load_certs(), bytes) + + +def test_get_api_key_returns_env_var_when_no_input(): + """Check that _get_api_key returns the env var when no input is provided.""" + os.environ[_DIODE_API_KEY_ENVVAR_NAME] = "env_var_key" + assert _get_api_key() == "env_var_key" + + +def test_get_api_key_returns_input_when_provided(): + """Check that _get_api_key returns the input when provided.""" + os.environ[_DIODE_API_KEY_ENVVAR_NAME] = "env_var_key" + assert _get_api_key("input_key") == "input_key" + + +def test_get_api_key_raises_error_when_no_input_or_env_var(): + """Check that _get_api_key raises an error when no input or env var is provided.""" + if _DIODE_API_KEY_ENVVAR_NAME in os.environ: + del os.environ[_DIODE_API_KEY_ENVVAR_NAME] + with pytest.raises(DiodeConfigError): + _get_api_key() + + +def test_parse_target_handles_http_prefix(): + """Check that parse_target raises an error when the target contains http://.""" + with pytest.raises(ValueError): + parse_target("http://localhost:8081") + + +def test_parse_target_handles_https_prefix(): + """Check that parse_target raises an error when the target contains https://.""" + with pytest.raises(ValueError): + parse_target("https://localhost:8081") + + +def test_parse_target_parses_authority_correctly(): + """Check that parse_target parses the authority correctly.""" + authority, path, tls_verify = parse_target("grpc://localhost:8081") + assert authority == "localhost:8081" + assert path == "" + assert tls_verify is False + + +def test_parse_target_adds_default_port_if_missing(): + """Check that parse_target adds the default port if missing.""" + authority, _, _ = parse_target("grpc://localhost") + assert authority == "localhost:443" + + +def test_parse_target_parses_path_correctly(): + """Check that parse_target parses the path correctly.""" + _, path, _ = parse_target("grpc://localhost:8081/my/path") + assert path == "/my/path" + + +def test_parse_target_handles_no_path(): + """Check that parse_target handles no path.""" + _, path, _ = parse_target("grpc://localhost:8081") + assert path == "" + + +def test_parse_target_parses_tls_verify_correctly(): + """Check that parse_target parses tls_verify correctly.""" + _, _, tls_verify = parse_target("grpcs://localhost:8081") + assert tls_verify is True + + +def test_get_sentry_dsn_returns_env_var_when_no_input(): + """Check that _get_sentry_dsn returns the env var when no input is provided.""" + os.environ[_DIODE_SENTRY_DSN_ENVVAR_NAME] = "env_var_dsn" + assert _get_sentry_dsn() == "env_var_dsn" + + +def test_get_sentry_dsn_returns_input_when_provided(): + """Check that _get_sentry_dsn returns the input when provided.""" + os.environ[_DIODE_SENTRY_DSN_ENVVAR_NAME] = "env_var_dsn" + assert _get_sentry_dsn("input_dsn") == "input_dsn" + + +def test_get_sentry_dsn_returns_none_when_no_input_or_env_var(): + """Check that _get_sentry_dsn returns None when no input or env var is provided.""" + if _DIODE_SENTRY_DSN_ENVVAR_NAME in os.environ: + del os.environ[_DIODE_SENTRY_DSN_ENVVAR_NAME] + assert _get_sentry_dsn() is None + + +def test_setup_sentry_initializes_with_correct_parameters(): + """Check that DiodeClient._setup_sentry() initializes with the correct parameters.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with mock.patch("sentry_sdk.init") as mock_init: + client._setup_sentry("https://user@password.mock.dsn/123456", 0.5, 0.5) + mock_init.assert_called_once_with( + dsn="https://user@password.mock.dsn/123456", + release=client.version, + traces_sample_rate=0.5, + profiles_sample_rate=0.5, + ) + + +def test_client_sets_up_secure_channel_when_grpcs_scheme_is_found_in_target(): + """Check that DiodeClient.__init__() sets up the gRPC secure channel when grpcs:// scheme is found in the target.""" + client = DiodeClient( + target="grpcs://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with ( + mock.patch("grpc.secure_channel") as mock_secure_channel, + mock.patch("logging.Logger.debug") as mock_debug, + ): + client.__init__( + target="grpcs://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + + mock_debug.assert_called_once_with("Setting up gRPC secure channel") + mock_secure_channel.assert_called_once() + + +def test_client_sets_up_insecure_channel_when_grpc_scheme_is_found_in_target(): + """Check that DiodeClient.__init__() sets up the gRPC insecure channel when grpc:// scheme is found in the target.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with ( + mock.patch("grpc.insecure_channel") as mock_insecure_channel, + mock.patch("logging.Logger.debug") as mock_debug, + ): + client.__init__( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + + mock_debug.assert_called_with( + "Setting up gRPC insecure channel", + ) + mock_insecure_channel.assert_called_once() + + +def test_client_interceptor_setup_with_path(): + """Check that DiodeClient.__init__() sets up the gRPC interceptor when a path is provided.""" + client = DiodeClient( + target="grpc://localhost:8081/my-path", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with ( + mock.patch("grpc.intercept_channel") as mock_intercept_channel, + mock.patch("logging.Logger.debug") as mock_debug, + ): + client.__init__( + target="grpc://localhost:8081/my-path", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + + mock_debug.assert_called_with( + "Setting up gRPC interceptor for path: /my-path", + ) + mock_intercept_channel.assert_called_once() + + +def test_client_interceptor_not_setup_without_path(): + """Check that DiodeClient.__init__() does not set up the gRPC interceptor when no path is provided.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with ( + mock.patch("grpc.intercept_channel") as mock_intercept_channel, + mock.patch("logging.Logger.debug") as mock_debug, + ): + client.__init__( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + + mock_debug.assert_called_with( + "Setting up gRPC insecure channel", + ) + mock_intercept_channel.assert_not_called() + + +def test_client_setup_sentry_called_when_sentry_dsn_exists(): + """Check that DiodeClient._setup_sentry() is called when sentry_dsn exists.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + sentry_dsn="https://user@password.mock.dsn/123456", + ) + with mock.patch.object(client, "_setup_sentry") as mock_setup_sentry: + client.__init__( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + sentry_dsn="https://user@password.mock.dsn/123456", + ) + mock_setup_sentry.assert_called_once_with( + "https://user@password.mock.dsn/123456", 1.0, 1.0 + ) + + +def test_client_setup_sentry_not_called_when_sentry_dsn_not_exists(): + """Check that DiodeClient._setup_sentry() is not called when sentry_dsn does not exist.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with mock.patch.object(client, "_setup_sentry") as mock_setup_sentry: + client.__init__( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + mock_setup_sentry.assert_not_called() + + +def test_client_properties_return_expected_values(): + """Check that DiodeClient properties return the expected values.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + assert client.name == "diode-sdk-python" + assert client.version == "0.0.1" + assert client.target == "localhost:8081" + assert client.path == "" + assert client.tls_verify is False + assert client.app_name == "my-producer" + assert client.app_version == "0.0.1" + assert isinstance(client.channel, grpc.Channel) + + +def test_client_enter_returns_self(): + """Check that DiodeClient.__enter__() returns self.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + assert client.__enter__() is client + + +def test_client_exit_closes_channel(): + """Check that DiodeClient.__exit__() closes the channel.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with mock.patch.object(client._channel, "close") as mock_close: + client.__exit__(None, None, None) + mock_close.assert_called_once() + + +def test_client_close_closes_channel(): + """Check that DiodeClient.close() closes the channel.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with mock.patch.object(client._channel, "close") as mock_close: + client.close() + mock_close.assert_called_once() + + +def test_setup_sentry_sets_correct_tags(): + """Check that DiodeClient._setup_sentry() sets the correct tags.""" + client = DiodeClient( + target="grpc://localhost:8081", + app_name="my-producer", + app_version="0.0.1", + api_key="abcde", + ) + with mock.patch("sentry_sdk.set_tag") as mock_set_tag: + client._setup_sentry("https://user@password.mock.dsn/123456", 0.5, 0.5) + mock_set_tag.assert_any_call("target", client.target) + mock_set_tag.assert_any_call("path", client.path if client.path else "/") + mock_set_tag.assert_any_call("app_name", client.app_name) + mock_set_tag.assert_any_call("app_version", client.app_version) + mock_set_tag.assert_any_call("sdk_version", client.version) + mock_set_tag.assert_any_call("platform", client._platform) + mock_set_tag.assert_any_call("python_version", client._python_version) + + +def test_interceptor_init_sets_subpath(): + """Check that DiodeMethodClientInterceptor.__init__() sets the subpath.""" + interceptor = DiodeMethodClientInterceptor("/my/path") + assert interceptor._subpath == "/my/path" + + +def test_interceptor_intercepts_unary_unary_calls(): + """Check that the interceptor intercepts unary unary calls.""" + interceptor = DiodeMethodClientInterceptor("/my/path") + + def continuation(x, _): + return x.method + + client_call_details = _ClientCallDetails( + "/diode.v1.IngesterService/Ingest", + None, + None, + None, + None, + None, + ) + request = None + assert ( + interceptor.intercept_unary_unary(continuation, client_call_details, request) + == "/my/path/diode.v1.IngesterService/Ingest" + ) + + +def test_interceptor_intercepts_stream_unary_calls(): + """Check that DiodeMethodClientInterceptor.intercept_stream_unary() intercepts stream unary calls.""" + interceptor = DiodeMethodClientInterceptor("/my/path") + + def continuation(x, _): + return x.method + + client_call_details = _ClientCallDetails( + "/diode.v1.IngesterService/Ingest", + None, + None, + None, + None, + None, + ) + request_iterator = None + assert ( + interceptor.intercept_stream_unary( + continuation, client_call_details, request_iterator + ) + == "/my/path/diode.v1.IngesterService/Ingest" + ) diff --git a/tests/test_ingester.py b/tests/test_ingester.py new file mode 100644 index 0000000..09b0398 --- /dev/null +++ b/tests/test_ingester.py @@ -0,0 +1,580 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs - Tests.""" + +# ruff: noqa: I001 +from netboxlabs.diode.sdk.diode.v1.ingester_pb2 import ( + Device as DevicePb, + DeviceType as DeviceTypePb, + Entity as EntityPb, + IPAddress as IPAddressPb, + Interface as InterfacePb, + Manufacturer as ManufacturerPb, + Platform as PlatformPb, + Prefix as PrefixPb, + Role as RolePb, + Site as SitePb, + Tag as TagPb, +) +from netboxlabs.diode.sdk.ingester import ( + Device, + DeviceType, + Entity, + IPAddress, + Interface, + Manufacturer, + Platform, + Prefix, + Role, + Site, + Tag, + convert_to_protobuf, +) + + +def test_convert_to_protobuf_returns_correct_class_when_value_is_string(): + """Check convert_to_protobuf returns correct class when value is string.""" + + class MockProtobufClass: + def __init__(self, name=None): + self.name = name + + result = convert_to_protobuf("test", MockProtobufClass, name="test") + assert isinstance(result, MockProtobufClass) + assert result.name == "test" + + +def test_convert_to_protobuf_returns_value_when_value_is_not_string(): + """Check convert_to_protobuf returns value when value is not string.""" + + class MockProtobufClass: + def __init__(self, name=None): + self.name = name + + mock_instance = MockProtobufClass(name="test") + result = convert_to_protobuf(mock_instance, MockProtobufClass) + assert result is mock_instance + + +def test_tag_instantiation_with_all_fields(): + """Check Tag instantiation with all fields.""" + tag = Tag(name="networking", slug="networking-slug", color="blue") + assert isinstance(tag, TagPb) + assert tag.name == "networking" + assert tag.slug == "networking-slug" + assert tag.color == "blue" + + +def test_tag_instantiation_with_only_name(): + """Check Tag instantiation with only name.""" + tag = Tag(name="networking") + assert isinstance(tag, TagPb) + assert tag.name == "networking" + assert tag.slug == "" + assert tag.color == "" + + +def test_tag_instantiation_with_no_fields(): + """Check Tag instantiation with no fields.""" + tag = Tag() + assert isinstance(tag, TagPb) + assert tag.name == "" + assert tag.slug == "" + assert tag.color == "" + + +def test_manufacturer_instantiation_with_tags_as_strings(): + """Check Manufacturer instantiation with tags as strings.""" + manufacturer = Manufacturer( + name="Cisco", + slug="cisco", + description="Networking equipment manufacturer", + tags=["networking", "equipment"], + ) + assert isinstance(manufacturer, ManufacturerPb) + assert manufacturer.name == "Cisco" + assert manufacturer.slug == "cisco" + assert manufacturer.description == "Networking equipment manufacturer" + assert len(manufacturer.tags) == 2 + for tag in manufacturer.tags: + assert isinstance(tag, TagPb) + + +def test_manufacturer_instantiation_with_tags_as_protobufs(): + """Check Manufacturer instantiation with tags as protobufs.""" + tags = [TagPb(name="networking"), TagPb(name="equipment")] + manufacturer = Manufacturer( + name="Cisco", + slug="cisco", + description="Networking equipment manufacturer", + tags=tags, + ) + assert isinstance(manufacturer, ManufacturerPb) + assert manufacturer.name == "Cisco" + assert manufacturer.slug == "cisco" + assert manufacturer.description == "Networking equipment manufacturer" + assert len(manufacturer.tags) == 2 + for tag in manufacturer.tags: + assert isinstance(tag, TagPb) + + +def test_platform_instantiation_with_all_fields(): + """Check Platform instantiation with all fields.""" + platform = Platform( + name="Platform1", + slug="platform1", + manufacturer="Manufacturer1", + description="This is a platform", + tags=["tag1", "tag2"], + ) + assert isinstance(platform, PlatformPb) + assert platform.name == "Platform1" + assert platform.slug == "platform1" + assert isinstance(platform.manufacturer, ManufacturerPb) + assert platform.manufacturer.name == "Manufacturer1" + assert platform.description == "This is a platform" + assert len(platform.tags) == 2 + for tag in platform.tags: + assert isinstance(tag, TagPb) + + +def test_platform_instantiation_with_explicit_manufacturer(): + """Check Platform instantiation with explicit Manufacturer.""" + platform = Platform( + name="Platform1", + slug="platform1", + manufacturer=Manufacturer(name="Manufacturer1"), + tags=["tag1", "tag2"], + ) + assert isinstance(platform, PlatformPb) + assert platform.name == "Platform1" + assert platform.slug == "platform1" + assert isinstance(platform.manufacturer, ManufacturerPb) + assert platform.manufacturer.name == "Manufacturer1" + assert len(platform.tags) == 2 + for tag in platform.tags: + assert isinstance(tag, TagPb) + + +def test_role_instantiation_with_all_fields(): + """Check Role instantiation with all fields.""" + role = Role( + name="Admin", + slug="admin", + color="blue", + description="Administrator role", + tags=["admin", "role"], + ) + assert isinstance(role, RolePb) + assert role.name == "Admin" + assert role.slug == "admin" + assert role.color == "blue" + assert role.description == "Administrator role" + assert len(role.tags) == 2 + for tag in role.tags: + assert isinstance(tag, TagPb) + + +def test_device_type_instantiation_with_all_fields(): + """Check DeviceType instantiation with all fields.""" + device_type = DeviceType( + model="Model1", + slug="model1", + manufacturer="Manufacturer1", + description="This is a device type", + comments="No comments", + part_number="1234", + tags=["tag1", "tag2"], + ) + assert isinstance(device_type, DeviceTypePb) + assert device_type.model == "Model1" + assert device_type.slug == "model1" + assert isinstance(device_type.manufacturer, ManufacturerPb) + assert device_type.manufacturer.name == "Manufacturer1" + assert device_type.description == "This is a device type" + assert device_type.comments == "No comments" + assert device_type.part_number == "1234" + assert len(device_type.tags) == 2 + for tag in device_type.tags: + assert isinstance(tag, TagPb) + + +def test_device_instantiation_with_all_fields(): + """Check Device instantiation with all fields.""" + device = Device( + name="Device1", + device_type="DeviceType1", + device_fqdn="device1.example.com", + role="Role1", + platform="Platform1", + serial="123456", + site="Site1", + asset_tag="123456", + status="active", + description="This is a device", + comments="No comments", + tags=["tag1", "tag2"], + primary_ip4="192.168.0.1", + primary_ip6="2001:db8::1", + manufacturer="Manufacturer1", + ) + assert isinstance(device, DevicePb) + assert device.name == "Device1" + assert isinstance(device.device_type, DeviceTypePb) + assert device.device_fqdn == "device1.example.com" + assert isinstance(device.role, RolePb) + assert isinstance(device.platform, PlatformPb) + assert device.serial == "123456" + assert isinstance(device.site, SitePb) + assert device.asset_tag == "123456" + assert device.status == "active" + assert device.description == "This is a device" + assert device.comments == "No comments" + assert len(device.tags) == 2 + for tag in device.tags: + assert isinstance(tag, TagPb) + assert isinstance(device.primary_ip4, IPAddressPb) + assert isinstance(device.primary_ip6, IPAddressPb) + + +def test_device_instantiation_with_explicit_nested_object_types(): + """Check Device instantiation with explicit nested object types.""" + device = Device( + name="Device1", + device_type=DeviceType(model="DeviceType1"), + device_fqdn="device1.example.com", + role=Role(name="Role1"), + platform=Platform(name="Platform1"), + serial="123456", + site=Site(name="Site1"), + asset_tag="123456", + status="active", + comments="No comments", + tags=["tag1", "tag2"], + primary_ip4="192.168.0.1", + primary_ip6="2001:db8::1", + manufacturer=Manufacturer(name="Manufacturer1"), + ) + assert isinstance(device, DevicePb) + assert isinstance(device.device_type, DeviceTypePb) + assert isinstance(device.role, RolePb) + assert isinstance(device.platform, PlatformPb) + assert isinstance(device.site, SitePb) + assert isinstance(device.primary_ip4, IPAddressPb) + assert isinstance(device.primary_ip6, IPAddressPb) + assert device.device_type.manufacturer.name == "Manufacturer1" + assert device.platform.manufacturer.name == "Manufacturer1" + + +def test_interface_instantiation_with_all_fields(): + """Check Interface instantiation with all fields.""" + interface = Interface( + name="Interface1", + device="Device1", + device_type="DeviceType1", + role="Role1", + platform="Platform1", + manufacturer="Manufacturer1", + site="Site1", + type="type1", + enabled=True, + mtu=1500, + mac_address="00:00:00:00:00:00", + speed=1000, + wwn="wwn1", + mgmt_only=True, + description="This is an interface", + mark_connected=True, + mode="mode1", + tags=["tag1", "tag2"], + ) + assert isinstance(interface, InterfacePb) + assert interface.name == "Interface1" + assert isinstance(interface.device, DevicePb) + assert interface.device.name == "Device1" + assert isinstance(interface.device.device_type, DeviceTypePb) + assert interface.device.device_type.model == "DeviceType1" + assert isinstance(interface.device.role, RolePb) + assert interface.device.role.name == "Role1" + assert isinstance(interface.device.platform, PlatformPb) + assert interface.device.platform.name == "Platform1" + assert isinstance(interface.device.platform.manufacturer, ManufacturerPb) + assert interface.device.platform.manufacturer.name == "Manufacturer1" + assert isinstance(interface.device.site, SitePb) + assert interface.device.site.name == "Site1" + assert interface.type == "type1" + assert interface.enabled is True + assert interface.mtu == 1500 + assert interface.mac_address == "00:00:00:00:00:00" + assert interface.speed == 1000 + assert interface.wwn == "wwn1" + assert interface.mgmt_only is True + assert interface.description == "This is an interface" + assert interface.mark_connected is True + assert interface.mode == "mode1" + assert len(interface.tags) == 2 + for tag in interface.tags: + assert isinstance(tag, TagPb) + + +def test_interface_instantiation_with_explicit_nested_object_types(): + """Check Interface instantiation with explicit nested object types.""" + interface = Interface( + name="Interface1", + device="Device1", + device_type=DeviceType(model="DeviceType1"), + role=Role(name="Role1"), + platform=Platform(name="Platform1"), + site=Site(name="Site1"), + manufacturer=Manufacturer(name="Manufacturer1"), + ) + assert isinstance(interface, InterfacePb) + assert isinstance(interface.device.device_type, DeviceTypePb) + assert isinstance(interface.device.role, RolePb) + assert isinstance(interface.device.platform, PlatformPb) + assert isinstance(interface.device.site, SitePb) + assert interface.device.platform.manufacturer.name == "Manufacturer1" + assert interface.device.device_type.manufacturer.name == "Manufacturer1" + + +def test_ip_address_instantiation_with_all_fields(): + """Check IPAddress instantiation with all fields.""" + ip_address = IPAddress( + address="192.168.0.1", + interface="Interface1", + device="Device1", + device_type="DeviceType1", + device_role="Role1", + platform="Platform1", + manufacturer="Manufacturer1", + site="Site1", + status="active", + role="admin", + dns_name="dns.example.com", + description="This is an IP address", + comments="No comments", + tags=["tag1", "tag2"], + ) + assert isinstance(ip_address, IPAddressPb) + assert ip_address.address == "192.168.0.1" + assert isinstance(ip_address.interface, InterfacePb) + assert ip_address.interface.name == "Interface1" + assert isinstance(ip_address.interface.device, DevicePb) + assert ip_address.interface.device.name == "Device1" + assert isinstance(ip_address.interface.device.device_type, DeviceTypePb) + assert ip_address.interface.device.device_type.model == "DeviceType1" + assert isinstance(ip_address.interface.device.role, RolePb) + assert ip_address.interface.device.role.name == "Role1" + assert isinstance(ip_address.interface.device.platform, PlatformPb) + assert ip_address.interface.device.platform.name == "Platform1" + assert isinstance(ip_address.interface.device.platform.manufacturer, ManufacturerPb) + assert ip_address.interface.device.platform.manufacturer.name == "Manufacturer1" + assert isinstance(ip_address.interface.device.site, SitePb) + assert ip_address.interface.device.site.name == "Site1" + assert ip_address.status == "active" + assert ip_address.role == "admin" + assert ip_address.dns_name == "dns.example.com" + assert ip_address.description == "This is an IP address" + assert ip_address.comments == "No comments" + assert len(ip_address.tags) == 2 + for tag in ip_address.tags: + assert isinstance(tag, TagPb) + + +def test_ip_address_instantiation_with_explicit_nested_object_types(): + """Check IPAddress instantiation with explicit nested object types.""" + ip_address = IPAddress( + address="192.168.0.1", + interface=Interface( + name="Interface1", + device=Device( + name="Device1", + device_type=DeviceType( + model="DeviceType1", manufacturer="Manufacturer1" + ), + role=Role(name="Role1"), + platform=Platform(name="Platform1", manufacturer="Manufacturer1"), + site=Site(name="Site1"), + ), + ), + status="active", + dns_name="dns.example.com", + description="This is an IP address", + comments="No comments", + tags=["tag1", "tag2"], + ) + assert isinstance(ip_address, IPAddressPb) + assert isinstance(ip_address.interface, InterfacePb) + assert isinstance(ip_address.interface.device, DevicePb) + assert isinstance(ip_address.interface.device.device_type, DeviceTypePb) + assert isinstance(ip_address.interface.device.role, RolePb) + assert isinstance(ip_address.interface.device.platform, PlatformPb) + assert isinstance(ip_address.interface.device.site, SitePb) + assert ip_address.interface.device.platform.manufacturer.name == "Manufacturer1" + assert ip_address.interface.device.device_type.manufacturer.name == "Manufacturer1" + assert ip_address.status == "active" + assert ip_address.dns_name == "dns.example.com" + assert ip_address.description == "This is an IP address" + assert ip_address.comments == "No comments" + assert len(ip_address.tags) == 2 + + +def test_ip_address_instantiation_with_manufacturer_populated_to_device_type_and_platform(): + """Check IPAddress instantiation with manufacturer populated to DeviceType and Platform.""" + ip_address = IPAddress( + address="192.168.0.1", + interface="Interface1", + device="Device1", + device_type=DeviceType(model="DeviceType1"), + device_role="Role1", + platform=Platform(name="Platform1"), + manufacturer="Manufacturer1", + site="Site1", + status="active", + role="admin", + dns_name="dns.example.com", + ) + assert isinstance(ip_address, IPAddressPb) + assert isinstance(ip_address.interface, InterfacePb) + assert isinstance(ip_address.interface.device, DevicePb) + assert isinstance(ip_address.interface.device.device_type, DeviceTypePb) + assert isinstance(ip_address.interface.device.role, RolePb) + assert isinstance(ip_address.interface.device.platform, PlatformPb) + assert isinstance(ip_address.interface.device.site, SitePb) + assert ip_address.interface.device.platform.manufacturer.name == "Manufacturer1" + assert ip_address.interface.device.device_type.manufacturer.name == "Manufacturer1" + assert ip_address.status == "active" + assert ip_address.dns_name == "dns.example.com" + + +def test_prefix_instantiation_with_all_fields(): + """Check Prefix instantiation with all fields.""" + prefix = Prefix( + prefix="192.168.0.0/24", + site="Site1", + status="active", + is_pool=True, + mark_utilized=False, + comments="No comments", + tags=["tag1", "tag2"], + ) + assert isinstance(prefix, PrefixPb) + assert prefix.prefix == "192.168.0.0/24" + assert isinstance(prefix.site, SitePb) + assert prefix.site.name == "Site1" + assert prefix.status == "active" + assert prefix.is_pool is True + assert prefix.mark_utilized is False + assert prefix.comments == "No comments" + assert len(prefix.tags) == 2 + for tag in prefix.tags: + assert isinstance(tag, TagPb) + + +def test_site_instantiation_with_all_fields(): + """Check Site instantiation with all fields.""" + site = Site( + name="Site1", + slug="site1", + status="active", + comments="No comments", + tags=["tag1", "tag2"], + ) + assert isinstance(site, SitePb) + assert site.name == "Site1" + assert site.slug == "site1" + assert site.status == "active" + assert site.comments == "No comments" + assert len(site.tags) == 2 + for tag in site.tags: + assert isinstance(tag, TagPb) + + +def test_entity_instantiation_with_site(): + """Check Entity instantiation with site.""" + entity = Entity( + site="Site1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.site, SitePb) + assert entity.site.name == "Site1" + + +def test_entity_instantiation_with_platform(): + """Check Entity instantiation with platform.""" + entity = Entity( + platform="Platform1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.platform, PlatformPb) + assert entity.platform.name == "Platform1" + + +def test_entity_instantiation_with_manufacturer(): + """Check Entity instantiation with manufacturer.""" + entity = Entity( + manufacturer="Manufacturer1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.manufacturer, ManufacturerPb) + assert entity.manufacturer.name == "Manufacturer1" + + +def test_entity_instantiation_with_device(): + """Check Entity instantiation with device.""" + entity = Entity( + device="Device1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.device, DevicePb) + assert entity.device.name == "Device1" + + +def test_entity_instantiation_with_device_type(): + """Check Entity instantiation with device type.""" + entity = Entity( + device_type="DeviceType1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.device_type, DeviceTypePb) + assert entity.device_type.model == "DeviceType1" + + +def test_entity_instantiation_with_role(): + """Check Entity instantiation with role.""" + entity = Entity( + device_role="Role1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.device_role, RolePb) + assert entity.device_role.name == "Role1" + + +def test_entity_instantiation_with_interface(): + """Check Entity instantiation with interface.""" + entity = Entity( + interface="Interface1", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.interface, InterfacePb) + assert entity.interface.name == "Interface1" + + +def test_entity_instantiation_with_ip_address(): + """Check Entity instantiation with IP address.""" + entity = Entity( + ip_address="192.168.0.1/24", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.ip_address, IPAddressPb) + assert entity.ip_address.address == "192.168.0.1/24" + + +def test_entity_instantiation_with_prefix(): + """Check Entity instantiation with prefix.""" + entity = Entity( + prefix="192.168.0.0/24", + ) + assert isinstance(entity, EntityPb) + assert isinstance(entity.prefix, PrefixPb) + assert entity.prefix.prefix == "192.168.0.0/24" diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..3f9a9ca --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# Copyright 2024 NetBox Labs Inc +"""NetBox Labs - Tests.""" + +from netboxlabs.diode.sdk.version import ( + __commit_hash__, + __track__, + __version__, + version_display, + version_semver, +) + + +def test_version_display_returns_correct_format(): + """Check the format of the version display.""" + expected = f"v{__version__}-{__track__}-{__commit_hash__}" + assert version_display() == expected + + +def test_version_display_returns_string(): + """Check that version display returns a string.""" + assert isinstance(version_display(), str) + + +def test_version_semver_returns_correct_version(): + """Check that version semver returns the correct version.""" + assert version_semver() == __version__ + + +def test_version_semver_returns_string(): + """Check that version semver returns a string.""" + assert isinstance(version_semver(), str)