├── MANIFEST.in ├── .tool-versions ├── src └── octodns_netbox_dns │ ├── __about__.py │ └── __init__.py ├── dev ├── .env.example ├── Dockerfile ├── zones │ ├── test.example.com.yaml │ ├── view.example.com.yaml │ ├── 1.168.192.in-addr.arpa.yaml │ └── example.com.yaml ├── sync2.yml ├── sync.yml ├── compose.yml └── configuration.py ├── renovate.json ├── .editorconfig ├── .github ├── workflows │ ├── check_code.yml │ ├── update_changelog.yml │ ├── build_pypackage.yml │ └── build_release.yml └── issue_template │ ├── feature_request.md │ └── bug_report.md ├── examples ├── netbox-to-yaml.yml └── netbox-to-cloudflare.yml ├── CONTRIBUTING.md ├── tests ├── test_make_absolute.py ├── test_escaple_semicolon.py └── test_records.py ├── justfile ├── .gitignore ├── README.md ├── pyproject.toml ├── LICENSE └── CHANGELOG.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | just 1.42.4 2 | -------------------------------------------------------------------------------- /src/octodns_netbox_dns/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.14" 2 | -------------------------------------------------------------------------------- /dev/.env.example: -------------------------------------------------------------------------------- 1 | # uid for docker containers 2 | UID=1000 3 | # gid for docker containers 4 | GID=1000 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>44net/renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /dev/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lscr.io/linuxserver/netbox:4.3.3 2 | 3 | RUN pip install -U \ 4 | wheel \ 5 | setuptools \ 6 | netbox-plugin-dns 7 | -------------------------------------------------------------------------------- /dev/zones/test.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? '' 3 | : ttl: 43200 4 | type: NS 5 | values: 6 | - ns1.example.com. 7 | - ns2.example.com. 8 | a1: 9 | type: A 10 | value: 192.168.1.11 11 | a2: 12 | type: A 13 | value: 192.168.1.12 14 | cname1: 15 | type: CNAME 16 | value: a1.test.example.com. 17 | -------------------------------------------------------------------------------- /dev/zones/view.example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? '' 3 | : ttl: 43200 4 | type: NS 5 | values: 6 | - ns1.example.com. 7 | - ns2.example.com. 8 | a1: 9 | type: A 10 | value: 192.168.1.11 11 | a2: 12 | type: A 13 | value: 192.168.1.13 14 | cname1: 15 | type: CNAME 16 | value: a1.view.example.com. 17 | -------------------------------------------------------------------------------- /dev/zones/1.168.192.in-addr.arpa.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? '' 3 | : ttl: 43200 4 | type: NS 5 | values: 6 | - ns1.example.com. 7 | - ns2.example.com. 8 | '1': 9 | type: PTR 10 | value: ns1.example.com. 11 | '2': 12 | type: PTR 13 | value: ns2.example.com. 14 | '11': 15 | type: PTR 16 | value: record11.example.com. 17 | '12': 18 | type: PTR 19 | value: record12.example.com. 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{yml,yaml,xml,hcl,tf}] 15 | indent_size = 2 16 | 17 | [*.{bat,cmd,ps1,ps[md]1}] 18 | end_of_line = crlf 19 | -------------------------------------------------------------------------------- /.github/workflows/check_code.yml: -------------------------------------------------------------------------------- 1 | name: check code 2 | 3 | on: 4 | push: 5 | branches: [main, master, dev] 6 | 7 | pull_request: 8 | branches: [main, master, dev] 9 | 10 | jobs: 11 | check-code: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout code 15 | uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.11" 20 | 21 | - name: install hatch 22 | run: pip install -U hatch 23 | 24 | - name: test codestyle 25 | run: hatch run lint:style 26 | 27 | - name: test typing 28 | run: hatch run lint:typing 29 | 30 | - name: run tests 31 | run: hatch run test:test 32 | -------------------------------------------------------------------------------- /.github/issue_template/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: feature-request 6 | assignees: olofvndrhr 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /dev/sync2.yml: -------------------------------------------------------------------------------- 1 | manager: 2 | max_workers: 1 3 | plan_outputs: 4 | html: 5 | class: octodns.provider.plan.PlanMarkdown 6 | 7 | providers: 8 | config: 9 | class: octodns.provider.yaml.YamlProvider 10 | directory: ./zones 11 | default_ttl: 3600 12 | enforce_order: true 13 | populate_should_replace: false 14 | 15 | netbox: 16 | class: octodns_netbox_dns.NetBoxDNSProvider 17 | url: http://localhost:8000 18 | token: 1ca8f8de1d651b0859052dc5e6a0858fd1e43e3d # change token for netbox 19 | view: false 20 | replace_duplicates: false 21 | make_absolute: true 22 | disable_ptr: true 23 | insecure_request: false 24 | 25 | zones: 26 | "*": 27 | sources: 28 | - config 29 | targets: 30 | - netbox 31 | -------------------------------------------------------------------------------- /examples/netbox-to-yaml.yml: -------------------------------------------------------------------------------- 1 | manager: 2 | max_workers: 2 3 | plan_outputs: 4 | html: 5 | class: octodns.provider.plan.PlanMarkdown 6 | 7 | processors: 8 | spf: 9 | class: octodns_spf.SpfDnsLookupProcessor 10 | 11 | providers: 12 | config: 13 | class: octodns.provider.yaml.YamlProvider 14 | directory: ./zones 15 | default_ttl: 3600 16 | enforce_order: true 17 | populate_should_replace: false 18 | netbox: 19 | class: octodns_netbox_dns.NetBoxDNSProvider 20 | url: https://netbox.example.net 21 | token: env/NETBOX_API_KEY 22 | view: false 23 | replace_duplicates: false 24 | make_absolute: true 25 | 26 | zones: 27 | "*": 28 | sources: 29 | - netbox 30 | processors: 31 | - spf 32 | targets: 33 | - config 34 | -------------------------------------------------------------------------------- /.github/workflows/update_changelog.yml: -------------------------------------------------------------------------------- 1 | name: update changelog 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | update-changelog: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | ref: main 17 | 18 | - name: install auto-changelog 19 | run: npm install auto-changelog 20 | 21 | - name: generate changelog 22 | run: >- 23 | npx auto-changelog -t keepachangelog 24 | --commit-limit 50 --backfill-limit 50 25 | --ignore-commit-pattern '[Bb]ump version|[Uu]pdate changelog|[Mm]erge pull request' 26 | 27 | - name: commit and push changelog 28 | uses: EndBug/add-and-commit@v9 29 | with: 30 | add: CHANGELOG.md 31 | message: "[bot] update changelog" 32 | -------------------------------------------------------------------------------- /.github/workflows/build_pypackage.yml: -------------------------------------------------------------------------------- 1 | name: build and release pypackage 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | pull_request: 9 | branches: [main, master] 10 | 11 | jobs: 12 | release-pypackage: 13 | runs-on: ubuntu-latest 14 | env: 15 | HATCH_INDEX_REPO: main 16 | HATCH_INDEX_USER: __token__ 17 | HATCH_INDEX_AUTH: ${{ secrets.PACKAGE_TOKEN }} 18 | steps: 19 | - name: checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: setup python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.11" 26 | 27 | - name: install hatch 28 | run: pip install -U hatch hatchling 29 | 30 | - name: build package 31 | run: hatch build --clean 32 | 33 | - name: publish package 34 | if: github.event_name != 'pull_request' 35 | run: hatch publish --yes --no-prompt 36 | -------------------------------------------------------------------------------- /.github/issue_template/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the tool 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: olofvndrhr 7 | 8 | --- 9 | 10 | **System info (please complete the following information):** 11 | 12 | - Host: [e.g. Linux/Debian or Docker] 13 | - `octodns-netbox-dns` version [e.g. v2.1.5] 14 | - `netbox-dns` version [e.g. v2.1.5] 15 | - Netbox version [e.g. v2.1.5] 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | Steps to reproduce the behavior: 22 | 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /dev/sync.yml: -------------------------------------------------------------------------------- 1 | manager: 2 | max_workers: 1 3 | plan_outputs: 4 | html: 5 | class: octodns.provider.plan.PlanMarkdown 6 | 7 | providers: 8 | config: 9 | class: octodns.provider.yaml.YamlProvider 10 | directory: ./zones 11 | default_ttl: 3600 12 | enforce_order: true 13 | populate_should_replace: false 14 | 15 | netbox: 16 | class: octodns_netbox_dns.NetBoxDNSProvider 17 | url: http://localhost:8000 18 | token: 1ca8f8de1d651b0859052dc5e6a0858fd1e43e3d # change token for netbox 19 | view: false 20 | replace_duplicates: false 21 | make_absolute: true 22 | disable_ptr: true 23 | insecure_request: false 24 | 25 | zones: 26 | example.com.: 27 | sources: 28 | - config 29 | targets: 30 | - netbox 31 | 32 | test.example.com.: 33 | sources: 34 | - config 35 | targets: 36 | - netbox 37 | 38 | view.example.com.: 39 | sources: 40 | - config 41 | targets: 42 | - netbox 43 | 44 | 1.168.192.in-addr.arpa.: 45 | sources: 46 | - config 47 | targets: 48 | - netbox 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # getting started 2 | 3 | ## with asdf 4 | 5 | ### tools needed 6 | 7 | - python 3.11 8 | - [hatch](https://hatch.pypa.io/) 9 | - [asdf](https://asdf-vm.com/guide/getting-started.html) 10 | 11 | ### setup 12 | 13 | 1. install [asdf](https://asdf-vm.com/guide/getting-started.html) 14 | 2. run `asdf install` in this directory to install all needed tools 15 | 3. run `just setup` to install the pre-commit hooks etc. 16 | 17 | ### pre-commit 18 | 19 | 1. run `just check` to lint the files and auto-format them. 20 | you can optionally run `just format` and `just lint` as a single action. 21 | 2. fix the issues which ruff reports 22 | 3. run `just build` to check if it builds correctly 23 | 4. commit changes 24 | 25 | ## manual 26 | 27 | ### tools needed 28 | 29 | - python 3.11 30 | - [hatch](https://hatch.pypa.io/) 31 | 32 | ### setup 33 | 34 | 1. install [just](https://github.com/casey/just) 35 | 2. run `just setup` to install the pre-commit hooks etc. 36 | 37 | ### pre-commit 38 | 39 | 1. run `hatch run lint:fmt` to lint the files and auto-format them. 40 | 2. fix the issues which ruff reports 41 | 3. run `hatch build --clean` to check if it builds correctly 42 | 4. commit changes 43 | -------------------------------------------------------------------------------- /tests/test_make_absolute.py: -------------------------------------------------------------------------------- 1 | from octodns_netbox_dns import NetBoxDNSProvider 2 | 3 | 4 | DEFAULT_CONFIG = { 5 | "id": 1, 6 | "url": "https://localhost:8000", 7 | "token": "", 8 | "view": False, 9 | "replace_duplicates": False, 10 | "make_absolute": True, 11 | } 12 | 13 | nbdns = NetBoxDNSProvider(**DEFAULT_CONFIG) # type:ignore 14 | 15 | 16 | def test_absolute() -> None: 17 | rcd = "example.com" 18 | absolute = nbdns._make_absolute(rcd) 19 | 20 | assert absolute == "example.com." 21 | 22 | 23 | def test_noop() -> None: 24 | rcd = "example.com." 25 | absolute = nbdns._make_absolute(rcd) 26 | 27 | assert absolute == "example.com." 28 | 29 | 30 | def test_disabled() -> None: 31 | args = {**DEFAULT_CONFIG, "make_absolute": False} 32 | nbdns = NetBoxDNSProvider(**args) # type:ignore 33 | rcd = "example.com" 34 | relative = nbdns._make_absolute(rcd, force=False) 35 | 36 | assert relative == "example.com" 37 | 38 | 39 | def test_force() -> None: 40 | args = {**DEFAULT_CONFIG, "make_absolute": False} 41 | nbdns = NetBoxDNSProvider(**args) # type:ignore 42 | rcd = "example.com" 43 | absolute = nbdns._make_absolute(rcd, force=True) 44 | 45 | assert absolute == "example.com." 46 | -------------------------------------------------------------------------------- /examples/netbox-to-cloudflare.yml: -------------------------------------------------------------------------------- 1 | manager: 2 | max_workers: 2 3 | plan_outputs: 4 | html: 5 | class: octodns.provider.plan.PlanMarkdown 6 | 7 | processors: 8 | spf: 9 | class: octodns_spf.SpfDnsLookupProcessor 10 | no-root-ns: 11 | class: octodns.processor.filter.IgnoreRootNsFilter 12 | ignore-non-public: 13 | class: octodns.processor.filter.NetworkValueRejectlistFilter 14 | rejectlist: 15 | - 127.0.0.0/8 # loopback 16 | - 192.168.0.0/16 # rfc1918 17 | - 172.16.0.0/12 # rfc1918 18 | - 10.0.0.0/8 # rfc1918 19 | - ::1/128 # loopback 20 | - fc00::/7 # ula 21 | - fe80::/10 # link-local 22 | - f00::/8 # multicast 23 | 24 | providers: 25 | netbox: 26 | class: octodns_netbox_dns.NetBoxDNSProvider 27 | url: https://netbox.example.net 28 | token: env/NETBOX_API_KEY 29 | view: false 30 | replace_duplicates: false 31 | make_absolute: true 32 | cloudflare: 33 | class: octodns_cloudflare.CloudflareProvider 34 | token: env/CLOUDFLARE_API_KEY 35 | account_id: env/CLOUDFLARE_ACCOUNT_ID 36 | cdn: false 37 | pagerules: false 38 | retry_count: 4 39 | retry_period: 300 40 | zones_per_page: 50 41 | records_per_page: 100 42 | 43 | zones: 44 | "*": 45 | sources: 46 | - netbox 47 | processors: 48 | - spf 49 | - no-root-ns 50 | - ignore-non-public 51 | targets: 52 | - cloudflare 53 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: build and create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | pull_request: 9 | branches: [main, master] 10 | 11 | jobs: 12 | release-github: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: setup python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.11" 24 | 25 | - name: install hatch 26 | run: pip install -U hatch hatchling 27 | 28 | - name: build package 29 | run: hatch build --clean 30 | 31 | - name: install auto-changelog 32 | run: npm install auto-changelog 33 | 34 | - name: generate changelog 35 | run: >- 36 | npx auto-changelog -t keepachangelog 37 | --commit-limit 50 --backfill-limit 50 38 | --ignore-commit-pattern '[Bb]ump version|[Uu]pdate changelog|[Mm]erge pull request' 39 | 40 | - name: get release notes 41 | id: release-notes 42 | uses: olofvndrhr/releasenote-gen@v1 43 | 44 | - name: create github release 45 | uses: ncipollo/release-action@v1 46 | if: github.event_name != 'pull_request' 47 | with: 48 | name: ${{ github.ref_name }} 49 | body: ${{ steps.release-notes.outputs.releasenotes }} 50 | artifacts: |- 51 | dist/** 52 | -------------------------------------------------------------------------------- /dev/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: netbox-dev:build 4 | container_name: netbox-dev-app 5 | restart: unless-stopped 6 | security_opt: ["no-new-privileges:true"] 7 | build: 8 | dockerfile: Dockerfile 9 | depends_on: 10 | - db 11 | - redis 12 | networks: 13 | - appnet 14 | ports: 15 | - "8000:8000" 16 | volumes: 17 | - ./netbox-data/:/config/:rw 18 | - ./configuration.py:/config/configuration.py:ro 19 | environment: 20 | - TZ=Europe/Zurich 21 | - PUID=${UID} 22 | - PGID=${GID} 23 | - DB_HOST=netbox-dev-db 24 | - DB_NAME=netbox 25 | - DB_USER=netbox 26 | - DB_PASSWORD=netbox-dev 27 | - REDIS_HOST=netbox-dev-redis 28 | - SUPERUSER_EMAIL=admin@example.com 29 | - SUPERUSER_PASSWORD=netbox-dev 30 | 31 | db: 32 | image: bitnami/postgresql:14.9.0 33 | container_name: netbox-dev-db 34 | restart: unless-stopped 35 | user: ${UID} 36 | security_opt: ["no-new-privileges:true"] 37 | networks: 38 | - appnet 39 | volumes: 40 | - /etc/localtime:/etc/localtime:ro 41 | - ./db-data/:/bitnami/postgresql/:rw 42 | environment: 43 | - TZ=Europe/Zurich 44 | - POSTGRESQL_POSTGRES_PASSWORD=netbox-dev 45 | - POSTGRES_DB=netbox 46 | - POSTGRES_USER=netbox 47 | - POSTGRES_PASSWORD=netbox-dev 48 | 49 | redis: 50 | image: bitnami/redis:7.2 51 | container_name: netbox-dev-redis 52 | restart: unless-stopped 53 | user: ${UID} 54 | security_opt: ["no-new-privileges:true"] 55 | networks: 56 | - appnet 57 | volumes: 58 | - ./redis-data/:/bitnami/redis/data/:rw 59 | environment: 60 | - TZ=Europe/Zurich 61 | - ALLOW_EMPTY_PASSWORD=yes 62 | 63 | networks: 64 | appnet: 65 | name: netbox-dev 66 | driver: bridge 67 | -------------------------------------------------------------------------------- /tests/test_escaple_semicolon.py: -------------------------------------------------------------------------------- 1 | from octodns_netbox_dns import NetBoxDNSProvider 2 | 3 | 4 | DEFAULT_CONFIG = { 5 | "id": 1, 6 | "url": "https://localhost:8000", 7 | "token": "", 8 | "make_absolute": True, 9 | } 10 | nbdns = NetBoxDNSProvider(**DEFAULT_CONFIG) # type:ignore 11 | 12 | 13 | def test_escape1() -> None: 14 | rcd_value = r"v=TLSRPTv1; rua=mailto:tlsrpt@example.com" 15 | value = nbdns._escape_semicolon(rcd_value) 16 | 17 | assert value == r"v=TLSRPTv1\; rua=mailto:tlsrpt@example.com" 18 | 19 | 20 | def test_escape2() -> None: 21 | rcd_value = r"v=TLSRPTv1\; rua=mailto:tlsrpt@example.com" 22 | value = nbdns._escape_semicolon(rcd_value) 23 | 24 | assert value == r"v=TLSRPTv1\\; rua=mailto:tlsrpt@example.com" 25 | 26 | 27 | def test_escape3() -> None: 28 | rcd_value = r"t=y\;o=~\;" 29 | value = nbdns._escape_semicolon(rcd_value) 30 | 31 | assert value == r"t=y\\;o=~\\;" 32 | 33 | 34 | def test_escape4() -> None: 35 | rcd_value = r"t=y;o=~;" 36 | value = nbdns._escape_semicolon(rcd_value) 37 | 38 | assert value == r"t=y\;o=~\;" 39 | 40 | 41 | def test_unescape1() -> None: 42 | rcd_value = r"v=TLSRPTv1\; rua=mailto:tlsrpt@example.com" 43 | value = nbdns._unescape_semicolon(rcd_value) 44 | 45 | assert value == r"v=TLSRPTv1; rua=mailto:tlsrpt@example.com" 46 | 47 | 48 | def test_unescape2() -> None: 49 | rcd_value = r"v=TLSRPTv1\\; rua=mailto:tlsrpt@example.com" 50 | value = nbdns._unescape_semicolon(rcd_value) 51 | 52 | assert value == r"v=TLSRPTv1; rua=mailto:tlsrpt@example.com" 53 | 54 | 55 | def test_unescape3() -> None: 56 | rcd_value = r"t=y\\;o=~\;" 57 | value = nbdns._unescape_semicolon(rcd_value) 58 | 59 | assert value == r"t=y;o=~;" 60 | 61 | 62 | def test_unescape4() -> None: 63 | rcd_value = r"t=y;o=~;" 64 | value = nbdns._unescape_semicolon(rcd_value) 65 | 66 | assert value == r"t=y;o=~;" 67 | -------------------------------------------------------------------------------- /dev/zones/example.com.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ? '' 3 | : ttl: 43200 4 | type: NS 5 | values: 6 | - ns1.example.com. 7 | - ns2.example.com. 8 | _srv1._tcp: 9 | type: SRV 10 | value: 11 | port: 9987 12 | priority: 0 13 | target: example.com. 14 | weight: 5 15 | a1: 16 | type: A 17 | value: 192.168.1.11 18 | a2: 19 | type: A 20 | value: 192.168.1.12 21 | aaaa1: 22 | type: AAAA 23 | value: 2001:db8::8a2e:370:7334 24 | aaaa2: 25 | type: AAAA 26 | value: 2001:db8::8a2e:370:7335 27 | abc: 28 | type: CNAME 29 | value: def.example.com. 30 | caa1: 31 | type: CAA 32 | value: 33 | flags: 0 34 | tag: issue 35 | value: letsencrypt.org 36 | cname1: 37 | type: CNAME 38 | value: a1.example.com. 39 | cname2: 40 | type: CNAME 41 | value: a1.test.example.com. 42 | cname3: 43 | type: CNAME 44 | value: a1.view.example.com. 45 | dname1: 46 | type: DNAME 47 | value: example.net. 48 | loc1: 49 | type: LOC 50 | value: 51 | altitude: 1.0 52 | lat_degrees: 0 53 | lat_direction: N 54 | lat_minutes: 0 55 | lat_seconds: 0.0 56 | long_degrees: 0 57 | long_direction: W 58 | long_minutes: 0 59 | long_seconds: 0.0 60 | precision_horz: 10000.0 61 | precision_vert: 10.0 62 | size: 1.0 63 | mx1: 64 | type: MX 65 | value: 66 | exchange: mx.example.com. 67 | preference: 10 68 | record1: 69 | type: CNAME 70 | value: record11.example.com. 71 | sshfp1: 72 | type: SSHFP 73 | value: 74 | algorithm: 4 75 | fingerprint: 123456789abcdef67890123456789abcdef67890123456789abcdef123456789 76 | fingerprint_type: 2 77 | txt1: 78 | type: TXT 79 | value: v=TLSRPTv1\; rua=mailto:tlsrpt@example.com 80 | txt2: 81 | type: TXT 82 | value: v=TLSRPTv1\\; rua=mailto:tlsrpt@example.com 83 | txt3: 84 | type: TXT 85 | value: v=DKIM1\; k=rsa\; p=/0f+sikE+k9ZKbn1BJu0/soWht/+Zd/nc/+Gy//mQ1B5sCKYKgAmYTSWkxRjFzkc6KAQhi+/IzaFogEV050wcscdC8Rc8lAQzDUFrMs2ZZK1vFtkwIDAQAB 86 | txt4: 87 | type: TXT 88 | value: t=y\\;o=~\\; 89 | txt5: 90 | type: TXT 91 | value: t=y\;o=~\; 92 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | default: show_receipts 4 | 5 | set shell := ["bash", "-uc"] 6 | set dotenv-load := true 7 | 8 | show_receipts: 9 | just --list 10 | 11 | show_system_info: 12 | @echo "==================================" 13 | @echo "os : {{ os() }}" 14 | @echo "arch: {{ arch() }}" 15 | @echo "justfile dir: {{ justfile_directory() }}" 16 | @echo "invocation dir: {{ invocation_directory() }}" 17 | @echo "running dir: `pwd -P`" 18 | @echo "==================================" 19 | 20 | setup: 21 | asdf install 22 | 23 | create_venv: 24 | @echo "creating venv" 25 | python3 -m pip install --upgrade pip setuptools wheel 26 | python3 -m venv venv 27 | 28 | install_deps: 29 | @echo "installing dependencies" 30 | python3 -m hatch dep show requirements --project-only > /tmp/requirements.txt 31 | pip3 install -r /tmp/requirements.txt 32 | 33 | install_deps_dev: 34 | @echo "installing dev dependencies" 35 | python3 -m hatch dep show requirements --project-only > /tmp/requirements.txt 36 | python3 -m hatch dep show requirements --env-only >> /tmp/requirements.txt 37 | pip3 install -r /tmp/requirements.txt 38 | 39 | create_reqs: 40 | @echo "creating requirements" 41 | pipreqs --force --savepath requirements.txt src/octodns_netbox_dns 42 | 43 | lint *args: 44 | just show_system_info 45 | hatch run lint:style {{ args }} 46 | hatch run lint:typing {{ args }} 47 | 48 | format *args: 49 | just show_system_info 50 | hatch run lint:fmt {{ args }} 51 | 52 | check *args: 53 | just lint {{ args }} 54 | just format {{ args }} 55 | 56 | build *args: 57 | hatch build --clean {{ args }} 58 | 59 | test *args: 60 | hatch run test:test {{ args }} 61 | 62 | up: 63 | docker compose -f dev/compose.yml build 64 | docker compose -f dev/compose.yml up 65 | 66 | down: 67 | docker compose -f dev/compose.yml down 68 | 69 | clean: 70 | rm -rf dev/db-data/* 71 | rm -rf dev/redis-data/* 72 | rm -rf dev/netbox-data/* 73 | 74 | sync *args: 75 | hatch -v run test:sync {{ args }} 76 | 77 | sync2 *args: 78 | hatch -v run test:sync {{ args }} 79 | 80 | dump *args: 81 | hatch -v run test:dump {{ args }} 82 | 83 | dump2 *args: 84 | hatch -v run test:dump2 {{ args }} 85 | 86 | validate *args: 87 | hatch -v run test:validate {{ args }} 88 | 89 | validate2 *args: 90 | hatch -v run test:validate {{ args }} 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | test.ipynb 4 | test.py 5 | test.sh 6 | downloads/ 7 | venv 8 | .mypy_cache/ 9 | .ruff_cache/ 10 | __pycache__/ 11 | .pytest_cache/ 12 | .hatch/ 13 | 14 | # dev 15 | dev/db-data 16 | dev/redis-data 17 | dev/netbox-data 18 | dev/output 19 | 20 | .stignore 21 | 22 | ### Python template 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | share/python-wheels/ 46 | *.egg-info/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | *.py,cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | cover/ 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | local_settings.py 83 | db.sqlite3 84 | db.sqlite3-journal 85 | 86 | # Flask stuff: 87 | instance/ 88 | .webassets-cache 89 | 90 | # Scrapy stuff: 91 | .scrapy 92 | 93 | # Sphinx documentation 94 | docs/_build/ 95 | 96 | # PyBuilder 97 | .pybuilder/ 98 | target/ 99 | 100 | # Jupyter Notebook 101 | .ipynb_checkpoints 102 | 103 | # IPython 104 | profile_default/ 105 | ipython_config.py 106 | 107 | # pyenv 108 | # For a library or package, you might want to ignore these files since the code is 109 | # intended to run in multiple environments; otherwise, check them in: 110 | # .python-version 111 | 112 | # pipenv 113 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 114 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 115 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 116 | # install all needed dependencies. 117 | #Pipfile.lock 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netbox-plugin-dns provider for octodns 2 | 3 | [octodns](https://github.com/octodns/octodns) provider for [netbox-plugin-dns](https://github.com/peteeckel/netbox-plugin-dns) 4 | 5 | > syncs dns records from and to netbox via [octodns](https://github.com/octodns/octodns) 6 | 7 | ## config 8 | 9 | ```yml 10 | providers: 11 | config: 12 | class: octodns_netbox_dns.NetBoxDNSProvider 13 | # Netbox instance url 14 | # [mandatory] 15 | url: "https://some-url" 16 | # Netbox API token 17 | # [mandatory] 18 | token: env/NETBOX_API_KEY 19 | # Filter by zone view. Can either be the name of the view, or "null". 20 | # "null" -> do not filter by view. 21 | # [optional, default=null] 22 | view: null 23 | # When records sourced from multiple providers, allows provider 24 | # to replace entries coming from the previous one. 25 | # Implementation matches YamlProvider's 'populate_should_replace' 26 | # [optional, default=false] 27 | replace_duplicates: false 28 | # Make CNAME, MX and SRV records absolute if they are missing the trailing "." 29 | # [optional, default=false] 30 | make_absolute: false 31 | # Disable automatic PTR record creating in the NetboxDNS plugin. 32 | # [optional, default=true] 33 | disable_ptr: true 34 | # Disable certificate verification for unsecure https. 35 | # [optional, default=false] 36 | insecure_request: false 37 | # Only include zones with this status when dynamic zones are used, e.g. "*". 38 | # [optional, default=active] 39 | zone_status_filter: active 40 | # Only include records with this status when records are listed from a zone. 41 | # [optional, default=active] 42 | record_status_filter: active 43 | # Maximal page size of queries. 44 | # A value of 0 means: show every item. Can cause errors with the NetBox setting: MAX_PAGE_SIZE 45 | # [optional, default=0] 46 | max_page_size: 0 47 | ``` 48 | 49 | ## compatibility 50 | 51 | > actively tested on the newest `netbox-plugin-dns` and `netbox` versions 52 | 53 | | provider | [netbox-plugin-dns](https://github.com/peteeckel/netbox-plugin-dns) | [netbox](https://github.com/netbox-community/netbox) | 54 | | ------------ | ------------------------------------------------------------------- | ---------------------------------------------------- | 55 | | `>= v0.3.3` | `>=0.21.0` | `>=3.6.0` | 56 | | `>= v0.3.6` | `>=1.0.0` | `>=4.0.0` | 57 | | `>= v0.3.11` | `>=1.2.3` | `>=4.2.0` | 58 | 59 | ## limitations 60 | 61 | the records can only be synced to netbox-dns if the zone is already existing. 62 | the provider _CAN NOT_ create zones (as of now). 63 | 64 | ## install 65 | 66 | ### via pip 67 | 68 | ```bash 69 | pip install octodns-netbox-dns 70 | ``` 71 | 72 | ### via pip + git 73 | 74 | ```bash 75 | pip install octodns-netbox-dns@git+https://github.com/olofvndrhr/octodns-netbox-dns.git@main 76 | ``` 77 | 78 | ### via pip + `requirements.txt` 79 | 80 | add the following line to your requirements file 81 | 82 | ```bash 83 | octodns-netbox-dns@git+https://github.com/olofvndrhr/octodns-netbox-dns.git@main 84 | ``` 85 | -------------------------------------------------------------------------------- /tests/test_records.py: -------------------------------------------------------------------------------- 1 | from octodns_netbox_dns import NetBoxDNSProvider 2 | 3 | 4 | DEFAULT_CONFIG = { 5 | "id": 1, 6 | "url": "https://localhost:8000", 7 | "token": "", 8 | "view": False, 9 | "replace_duplicates": False, 10 | "make_absolute": True, 11 | } 12 | 13 | nbdns = NetBoxDNSProvider(**DEFAULT_CONFIG) # type:ignore 14 | 15 | 16 | def test_a() -> None: 17 | rcd_type = "A" 18 | rcd_value = "127.0.0.1" 19 | value = nbdns._format_rdata(rcd_type, rcd_value) 20 | 21 | assert value == "127.0.0.1" 22 | 23 | 24 | def test_aaaa() -> None: 25 | rcd_type = "AAAA" 26 | rcd_value = "fc07::1" 27 | value = nbdns._format_rdata(rcd_type, rcd_value) 28 | 29 | assert value == "fc07::1" 30 | 31 | 32 | def test_alias() -> None: 33 | pass # not supported 34 | 35 | 36 | def test_caa() -> None: 37 | rcd_type = "CAA" 38 | rcd_value = '0 issue "letsencrypt.org"' 39 | value = nbdns._format_rdata(rcd_type, rcd_value) 40 | 41 | assert value == { 42 | "flags": 0, 43 | "tag": "issue", 44 | "value": "letsencrypt.org", 45 | } 46 | 47 | 48 | def test_cname() -> None: 49 | rcd_type = "CNAME" 50 | rcd_value = "test.example.com." 51 | value = nbdns._format_rdata(rcd_type, rcd_value) 52 | 53 | assert value == "test.example.com." 54 | 55 | 56 | def test_dname() -> None: 57 | rcd_type = "DNAME" 58 | rcd_value = "example.com." 59 | value = nbdns._format_rdata(rcd_type, rcd_value) 60 | 61 | assert value == "example.com." 62 | 63 | 64 | def test_ds() -> None: 65 | pass # not supported 66 | 67 | 68 | def test_loc() -> None: 69 | rcd_type = "LOC" 70 | rcd_value = "0 0 0 N 0 0 0 W 1 1 10000 10" 71 | value = nbdns._format_rdata(rcd_type, rcd_value) 72 | 73 | assert value == { 74 | "lat_degrees": 0, 75 | "lat_minutes": 0, 76 | "lat_seconds": 0.0, 77 | "lat_direction": "N", 78 | "long_degrees": 0, 79 | "long_minutes": 0, 80 | "long_seconds": 0.0, 81 | "long_direction": "W", 82 | "altitude": 1.0, 83 | "size": 1.0, 84 | "precision_horz": 10000.0, 85 | "precision_vert": 10.0, 86 | } 87 | 88 | 89 | def test_mx() -> None: 90 | rcd_type = "MX" 91 | rcd_value = "10 mx.example.com" 92 | value = nbdns._format_rdata(rcd_type, rcd_value) 93 | 94 | assert value == { 95 | "preference": 10, 96 | "exchange": "mx.example.com.", 97 | } 98 | 99 | 100 | def test_naptr() -> None: 101 | pass # not supported 102 | 103 | 104 | def test_ns() -> None: 105 | rcd_type = "NS" 106 | rcd_value = "ns.example.com." 107 | value = nbdns._format_rdata(rcd_type, rcd_value) 108 | 109 | assert value == "ns.example.com." 110 | 111 | 112 | def test_ptr() -> None: 113 | rcd_type = "PTR" 114 | rcd_value = "host.example.com." 115 | value = nbdns._format_rdata(rcd_type, rcd_value) 116 | 117 | assert value == "host.example.com." 118 | 119 | 120 | def test_spf() -> None: 121 | pass # not supported 122 | 123 | 124 | def test_srv() -> None: 125 | rcd_type = "SRV" 126 | rcd_value = r"0 5 25565 mc.example.com" 127 | value = nbdns._format_rdata(rcd_type, rcd_value) 128 | 129 | assert value == { 130 | "priority": 0, 131 | "weight": 5, 132 | "port": 25565, 133 | "target": "mc.example.com.", 134 | } 135 | 136 | 137 | def test_sshfp() -> None: 138 | rcd_type = "SSHFP" 139 | rcd_value = "4 2 123456789abcdef67890123456789abcdef67890123456789abcdef123456789" 140 | value = nbdns._format_rdata(rcd_type, rcd_value) 141 | 142 | assert value == { 143 | "algorithm": 4, 144 | "fingerprint_type": 2, 145 | "fingerprint": "123456789abcdef67890123456789abcdef67890123456789abcdef123456789", 146 | } 147 | 148 | 149 | def test_tlsa() -> None: 150 | pass # not supported 151 | 152 | 153 | def test_txt1() -> None: 154 | rcd_type = "TXT" 155 | rcd_value = "v=TLSRPTv1; rua=mailto:tlsrpt@example.com" 156 | value = nbdns._format_rdata(rcd_type, rcd_value) 157 | 158 | assert value == r"v=TLSRPTv1\; rua=mailto:tlsrpt@example.com" 159 | 160 | 161 | def test_txt2() -> None: 162 | rcd_type = "TXT" 163 | rcd_value = r"v=TLSRPTv1\; rua=mailto:tlsrpt@example.com" 164 | value = nbdns._format_rdata(rcd_type, rcd_value) 165 | 166 | assert value == r"v=TLSRPTv1\\; rua=mailto:tlsrpt@example.com" 167 | 168 | 169 | def test_txt3() -> None: 170 | rcd_type = "TXT" 171 | rcd_value = r"v=DKIM1; k=rsa; p=/0f+sikE+k9ZKbn1BJu0/soWht/+Zd/nc/+Gy//mQ1B5sCKYKgAmYTSWkxRjFzkc6KAQhi+/IzaFogEV050wcscdC8Rc8lAQzDUFrMs2ZZK1vFtkwIDAQAB" 172 | value = nbdns._format_rdata(rcd_type, rcd_value) 173 | 174 | assert ( 175 | value 176 | == r"v=DKIM1\; k=rsa\; p=/0f+sikE+k9ZKbn1BJu0/soWht/+Zd/nc/+Gy//mQ1B5sCKYKgAmYTSWkxRjFzkc6KAQhi+/IzaFogEV050wcscdC8Rc8lAQzDUFrMs2ZZK1vFtkwIDAQAB" 177 | ) 178 | 179 | 180 | def test_txt4() -> None: 181 | rcd_type = "TXT" 182 | rcd_value = r"t=y\;o=~\;" 183 | value = nbdns._format_rdata(rcd_type, rcd_value) 184 | 185 | assert value == r"t=y\\;o=~\\;" 186 | 187 | 188 | def test_txt5() -> None: 189 | rcd_type = "TXT" 190 | rcd_value = r"t=y;o=~;" 191 | value = nbdns._format_rdata(rcd_type, rcd_value) 192 | 193 | assert value == r"t=y\;o=~\;" 194 | 195 | 196 | def test_urlfwd() -> None: 197 | pass # not supported 198 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling==1.27.0", 4 | "hatch-regex-commit==0.0.3", 5 | ] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "octodns-netbox-dns" 10 | description = "octodns netbox-dns provider" 11 | readme = "README.md" 12 | license = "MIT" 13 | requires-python = ">=3.11" 14 | dynamic = ["version"] 15 | authors = [ 16 | { name = "Jeffrey C. Ollie", email = "jeff@ocjtech.us" }, 17 | { name = "Ivan Schaller", email = "ivan@schaller.sh" }, 18 | ] 19 | classifiers = [ 20 | "Programming Language :: Python :: 3.11", 21 | ] 22 | keywords = [] 23 | dependencies = [ 24 | "pynetbox~=7.5.0", 25 | "dnspython~=2.7.0", 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://github.com/olofvndrhr/octodns-netbox-dns" 30 | History = "https://github.com/olofvndrhr/octodns-netbox-dns/commits/master" 31 | Tracker = "https://github.com/olofvndrhr/octodns-netbox-dns/issues" 32 | Source = "https://github.com/olofvndrhr/octodns-netbox-dns" 33 | 34 | [tool.hatch.version] 35 | source = "regex_commit" 36 | path = "src/octodns_netbox_dns/__about__.py" 37 | tag_sign = false 38 | 39 | [tool.hatch.build.targets.sdist] 40 | packages = ["src/octodns_netbox_dns"] 41 | 42 | [tool.hatch.build.targets.wheel] 43 | packages = ["src/octodns_netbox_dns"] 44 | 45 | ### 46 | ### envs 47 | ### 48 | 49 | [tool.hatch.envs.test] 50 | dependencies = [ 51 | "pytest==8.4.2", 52 | "coverage==7.10.5", 53 | "octodns~=1.13.0", 54 | "octodns-spf==1.0.0", 55 | ] 56 | 57 | [[tool.hatch.envs.test.matrix]] 58 | python = ["3.11"] 59 | 60 | [tool.hatch.envs.test.scripts] 61 | test = "pytest {args:tests}" 62 | test-cov = [ 63 | "coverage erase", 64 | "coverage run -m pytest {args:tests}", 65 | ] 66 | cov-report = [ 67 | "- coverage combine", 68 | "coverage report", 69 | "coverage xml", 70 | ] 71 | cov = ["test-cov", "cov-report"] 72 | 73 | sync = [ 74 | "cd dev && octodns-sync --debug --config-file sync.yml --force {args}", 75 | ] 76 | sync2 = [ 77 | "cd dev && octodns-sync --debug --config-file sync2.yml --force {args}", 78 | ] 79 | dump = [ 80 | "cd dev && octodns-dump --debug --config-file sync.yml --output-dir output {args} '*' netbox", 81 | ] 82 | dump2 = [ 83 | "cd dev && octodns-dump --debug --config-file sync2.yml --output-dir output {args} '*' netbox", 84 | ] 85 | validate = [ 86 | "cd dev && octodns-validate --debug --config-file sync.yml {args}", 87 | ] 88 | validate2 = [ 89 | "cd dev && octodns-validate --debug --config-file sync2.yml {args}", 90 | ] 91 | 92 | [tool.hatch.envs.lint] 93 | python = "3.11" 94 | detached = true 95 | dependencies = [ 96 | "mypy==1.17.1", 97 | "ruff==0.12.12", 98 | ] 99 | 100 | [tool.hatch.envs.lint.scripts] 101 | typing = "mypy --non-interactive --install-types {args:src/octodns_netbox_dns tests}" 102 | style = [ 103 | "ruff check --no-fix {args:src tests}", 104 | "ruff format --diff {args:src tests}", 105 | ] 106 | all = ["style", "typing"] 107 | fmt = [ 108 | "ruff check --fix {args:src tests}", 109 | "ruff format {args:src tests}", 110 | "style", 111 | ] 112 | 113 | ### 114 | ### ruff 115 | ### 116 | 117 | [tool.ruff] 118 | target-version = "py311" 119 | line-length = 100 120 | indent-width = 4 121 | fix = true 122 | show-fixes = true 123 | respect-gitignore = true 124 | src = ["src", "tests"] 125 | 126 | [tool.ruff.lint] 127 | select = ["ALL"] 128 | ignore = [ 129 | "ANN002", 130 | "ANN003", 131 | "ANN401", 132 | "BLE001", 133 | "COM812", 134 | "D100", 135 | "D102", 136 | "D103", 137 | "EM101", 138 | "ERA001", 139 | "FBT", 140 | "G004", 141 | "SLF001", 142 | "TRY003", 143 | "TRY301", 144 | ] 145 | fixable = ["ALL"] 146 | unfixable = [] 147 | 148 | [tool.ruff.format] 149 | quote-style = "double" 150 | indent-style = "space" 151 | skip-magic-trailing-comma = false 152 | line-ending = "lf" 153 | docstring-code-format = true 154 | 155 | [tool.ruff.lint.per-file-ignores] 156 | "__init__.py" = ["D104"] 157 | "__about__.py" = ["D104", "F841"] 158 | "tests/*" = [ 159 | "E501", 160 | "PLR2004", 161 | "S101", 162 | "TID252", 163 | "PGH003", 164 | ] 165 | 166 | [tool.ruff.lint.pyupgrade] 167 | keep-runtime-typing = true 168 | 169 | [tool.ruff.lint.isort] 170 | lines-after-imports = 2 171 | known-first-party = [ 172 | "octodns_netbox_dns", 173 | ] 174 | 175 | [tool.ruff.lint.flake8-tidy-imports] 176 | ban-relative-imports = "all" 177 | 178 | [tool.ruff.lint.pylint] 179 | max-branches = 24 180 | max-returns = 12 181 | max-statements = 100 182 | max-args = 15 183 | allow-magic-value-types = [ 184 | "bytes", 185 | "complex", 186 | "float", 187 | "int", 188 | "str", 189 | ] 190 | 191 | [tool.ruff.lint.mccabe] 192 | max-complexity = 15 193 | 194 | [tool.ruff.lint.pydocstyle] 195 | convention = "google" 196 | 197 | [tool.ruff.lint.pycodestyle] 198 | max-doc-length = 100 199 | 200 | ### 201 | ### mypy 202 | ### 203 | 204 | [tool.mypy] 205 | #plugins = ["pydantic.mypy"] 206 | follow_imports = "silent" 207 | warn_redundant_casts = true 208 | warn_unused_ignores = true 209 | disallow_any_generics = true 210 | check_untyped_defs = true 211 | no_implicit_reexport = true 212 | ignore_missing_imports = true 213 | warn_return_any = true 214 | pretty = true 215 | show_column_numbers = true 216 | show_error_codes = true 217 | show_error_context = true 218 | 219 | #[tool.pydantic-mypy] 220 | #init_forbid_extra = true 221 | #init_typed = true 222 | #warn_required_dynamic_aliases = true 223 | 224 | ### 225 | ### pytest 226 | ### 227 | 228 | [tool.pytest.ini_options] 229 | pythonpath = ["src"] 230 | addopts = "--color=yes --exitfirst --verbose -ra" 231 | filterwarnings = [ 232 | 'ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning', 233 | ] 234 | 235 | ### 236 | ### coverage 237 | ### 238 | 239 | [tool.coverage.run] 240 | source_pkgs = [ 241 | "octodns_netbox_dns", 242 | "tests", 243 | ] 244 | branch = true 245 | parallel = true 246 | omit = [ 247 | "src/octodns_netbox_dns/__about__.py", 248 | ] 249 | 250 | [tool.coverage.paths] 251 | octodns_netbox_dns = [ 252 | "src/octodns_netbox_dns", 253 | "*/octodns-netbox-dns/src/octodns_netbox_dns", 254 | ] 255 | tests = [ 256 | "tests", 257 | "*/octodns-netbox-dns/tests", 258 | ] 259 | 260 | [tool.coverage.report] 261 | # Regexes for lines to exclude from consideration 262 | exclude_lines = [ 263 | # Have to re-enable the standard pragma 264 | "pragma: no cover", 265 | # Don't complain about missing debug-only code: 266 | "def __repr__", 267 | "if self.debug", 268 | # Don't complain if tests don't hit defensive assertion code: 269 | "raise AssertionError", 270 | "raise NotImplementedError", 271 | # Don't complain if non-runnable code isn't run: 272 | "if 0:", 273 | "if __name__ == .__main__.:", 274 | # Don't complain about abstract methods, they aren't run: 275 | "@(abc.)?abstractmethod", 276 | "no cov", 277 | "if TYPE_CHECKING:", 278 | ] 279 | # ignore_errors = true 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | Copyright 2017 DigitalOcean -------------------------------------------------------------------------------- /dev/configuration.py: -------------------------------------------------------------------------------- 1 | ######################### 2 | # # 3 | # Required settings # 4 | # # 5 | ######################### 6 | 7 | PLUGINS = ["netbox_dns"] 8 | 9 | PLUGINS_CONFIG = { 10 | "netbox_dns": { 11 | "zone_default_ttl": 3600, 12 | "zone_soa_serial_auto": True, 13 | "zone_soa_minimum": 3600, 14 | "zone_nameservers": ["ns1.example.com", "ns2.example.com"], 15 | "zone_soa_mname": "ns1.example.com", 16 | "zone_soa_rname": "admin.example.com", 17 | "tolerate_underscores_in_labels": False, 18 | "tolerate_leading_underscore_types": [ 19 | "TXT", 20 | "SRV", 21 | ], 22 | "enable_root_zones": False, 23 | "enforce_unique_records": False, 24 | "feature_ipam_coupling": False, 25 | "feature_ipam_dns_info": True, 26 | }, 27 | } 28 | 29 | # This is a list of valid fully-qualified domain names (FQDNs) for the NetBox server. NetBox will not permit write 30 | # access to the server via any other hostnames. The first FQDN in the list will be treated as the preferred name. 31 | # 32 | # Example: ALLOWED_HOSTS = ['netbox.example.com', 'netbox.internal.local'] 33 | ALLOWED_HOSTS = ["localhost"] 34 | 35 | # PostgreSQL database configuration. See the Django documentation for a complete list of available parameters: 36 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 37 | DATABASE = { 38 | "NAME": "netbox", # Database name 39 | "USER": "netbox", # PostgreSQL username 40 | "PASSWORD": "netbox-dev", # PostgreSQL password 41 | "HOST": "netbox-dev-db", # Database server 42 | "PORT": "", # Database port (leave blank for default) 43 | "CONN_MAX_AGE": 300, # Max database connection age 44 | } 45 | 46 | # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate 47 | # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended 48 | # to use two separate database IDs. 49 | REDIS = { 50 | "tasks": { 51 | "HOST": "netbox-dev-redis", 52 | "PORT": 6379, 53 | # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel 54 | # 'SENTINELS': [('mysentinel.redis.example.com', 6379)], 55 | # 'SENTINEL_SERVICE': 'netbox', 56 | "PASSWORD": "", 57 | "DATABASE": 0, 58 | "SSL": False, 59 | # Set this to True to skip TLS certificate verification 60 | # This can expose the connection to attacks, be careful 61 | # 'INSECURE_SKIP_TLS_VERIFY': False, 62 | }, 63 | "caching": { 64 | "HOST": "netbox-dev-redis", 65 | "PORT": 6379, 66 | # Comment out `HOST` and `PORT` lines and uncomment the following if using Redis Sentinel 67 | # 'SENTINELS': [('mysentinel.redis.example.com', 6379)], 68 | # 'SENTINEL_SERVICE': 'netbox', 69 | "PASSWORD": "", 70 | "DATABASE": 1, 71 | "SSL": False, 72 | # Set this to True to skip TLS certificate verification 73 | # This can expose the connection to attacks, be careful 74 | # 'INSECURE_SKIP_TLS_VERIFY': False, 75 | }, 76 | } 77 | 78 | # This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file. 79 | # For optimal security, SECRET_KEY should be at least 50 characters in length and contain a mix of letters, numbers, and 80 | # symbols. NetBox will not run without this defined. For more information, see 81 | # https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SECRET_KEY 82 | SECRET_KEY = "ZFK2dO-pD@Vnjxf7gM@gh#jHwXm_wih%V6v@Byld7yX6c^Vsb!" 83 | 84 | ######################### 85 | # # 86 | # Optional settings # 87 | # # 88 | ######################### 89 | 90 | # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of 91 | # application errors (assuming correct email settings are provided). 92 | ADMINS = [ 93 | # ('John Doe', 'jdoe@example.com'), 94 | ] 95 | 96 | # URL schemes that are allowed within links in NetBox 97 | ALLOWED_URL_SCHEMES = ( 98 | "file", 99 | "ftp", 100 | "ftps", 101 | "http", 102 | "https", 103 | "irc", 104 | "mailto", 105 | "sftp", 106 | "ssh", 107 | "tel", 108 | "telnet", 109 | "tftp", 110 | "vnc", 111 | "xmpp", 112 | ) 113 | 114 | # Optionally display a persistent banner at the top and/or bottom of every page. HTML is allowed. To display the same 115 | # content in both banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP. 116 | BANNER_TOP = "" 117 | BANNER_BOTTOM = "" 118 | 119 | # Text to include on the login page above the login form. HTML is allowed. 120 | BANNER_LOGIN = "" 121 | 122 | # Base URL path if accessing NetBox within a directory. For example, if installed at https://example.com/netbox/, set: 123 | # BASE_PATH = 'netbox/' 124 | BASE_PATH = "" 125 | 126 | # Maximum number of days to retain logged changes. Set to 0 to retain changes indefinitely. (Default: 90) 127 | CHANGELOG_RETENTION = 90 128 | 129 | # API Cross-Origin Resource Sharing (CORS) settings. If CORS_ORIGIN_ALLOW_ALL is set to True, all origins will be 130 | # allowed. Otherwise, define a list of allowed origins using either CORS_ORIGIN_WHITELIST or 131 | # CORS_ORIGIN_REGEX_WHITELIST. For more information, see https://github.com/ottoyiu/django-cors-headers 132 | CORS_ORIGIN_ALLOW_ALL = False 133 | CORS_ORIGIN_WHITELIST = [ 134 | # 'https://hostname.example.com', 135 | ] 136 | CORS_ORIGIN_REGEX_WHITELIST = [ 137 | # r'^(https?://)?(\w+\.)?example\.com$', 138 | ] 139 | 140 | # Specify any custom validators here, as a mapping of model to a list of validators classes. Validators should be 141 | # instances of or inherit from CustomValidator. 142 | # from extras.validators import CustomValidator 143 | CUSTOM_VALIDATORS = { 144 | # 'dcim.site': [ 145 | # CustomValidator({ 146 | # 'name': { 147 | # 'min_length': 10, 148 | # 'regex': r'\d{3}$', 149 | # } 150 | # }) 151 | # ], 152 | } 153 | 154 | # Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal 155 | # sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging 156 | # on a production system. 157 | DEBUG = False 158 | 159 | # Email settings 160 | EMAIL = { 161 | "SERVER": "localhost", 162 | "PORT": 25, 163 | "USERNAME": "", 164 | "PASSWORD": "", 165 | "USE_SSL": False, 166 | "USE_TLS": False, 167 | "TIMEOUT": 10, # seconds 168 | "FROM_EMAIL": "", 169 | } 170 | 171 | # Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce unique IP space within the global table 172 | # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. 173 | ENFORCE_GLOBAL_UNIQUE = False 174 | 175 | # Exempt certain models from the enforcement of view permissions. Models listed here will be viewable by all users and 176 | # by anonymous users. List models in the form `.`. Add '*' to this list to exempt all models. 177 | EXEMPT_VIEW_PERMISSIONS = [ 178 | # 'dcim.site', 179 | # 'dcim.region', 180 | # 'ipam.prefix', 181 | ] 182 | 183 | # Enable the GraphQL API 184 | GRAPHQL_ENABLED = True 185 | 186 | # HTTP proxies NetBox should use when sending outbound HTTP requests (e.g. for webhooks). 187 | # HTTP_PROXIES = { 188 | # 'http': 'http://10.10.1.10:3128', 189 | # 'https': 'http://10.10.1.10:1080', 190 | # } 191 | 192 | # IP addresses recognized as internal to the system. The debugging toolbar will be available only to clients accessing 193 | # NetBox from an internal IP. 194 | INTERNAL_IPS = ("127.0.0.1", "::1") 195 | 196 | # Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs: 197 | # https://docs.djangoproject.com/en/stable/topics/logging/ 198 | LOGGING = {} 199 | 200 | # Automatically reset the lifetime of a valid session upon each authenticated request. Enables users to remain 201 | # authenticated to NetBox indefinitely. 202 | LOGIN_PERSISTENCE = False 203 | 204 | # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users 205 | # are permitted to access most data in NetBox but not make any changes. 206 | LOGIN_REQUIRED = False 207 | 208 | # The length of time (in seconds) for which a user will remain logged into the web UI before being prompted to 209 | # re-authenticate. (Default: 1209600 [14 days]) 210 | LOGIN_TIMEOUT = None 211 | 212 | # Setting this to True will display a "maintenance mode" banner at the top of every page. 213 | MAINTENANCE_MODE = False 214 | 215 | # The URL to use when mapping physical addresses or GPS coordinates 216 | MAPS_URL = "https://maps.google.com/?q=" 217 | 218 | # An API consumer can request an arbitrary number of objects =by appending the "limit" parameter to the URL (e.g. 219 | # "?limit=1000"). This setting defines the maximum limit. Setting it to 0 or None will allow an API consumer to request 220 | # all objects by specifying "?limit=0". 221 | MAX_PAGE_SIZE = 1000 222 | 223 | # The file path where uploaded media such as image attachments are stored. A trailing slash is not needed. Note that 224 | # the default value of this setting is derived from the installed location. 225 | # MEDIA_ROOT = '/opt/netbox/netbox/media' 226 | 227 | # By default uploaded media is stored on the local filesystem. Using Django-storages is also supported. Provide the 228 | # class path of the storage driver in STORAGE_BACKEND and any configuration options in STORAGE_CONFIG. For example: 229 | # STORAGE_BACKEND = 'storages.backends.s3boto3.S3Boto3Storage' 230 | # STORAGE_CONFIG = { 231 | # 'AWS_ACCESS_KEY_ID': 'Key ID', 232 | # 'AWS_SECRET_ACCESS_KEY': 'Secret', 233 | # 'AWS_STORAGE_BUCKET_NAME': 'netbox', 234 | # 'AWS_S3_REGION_NAME': 'eu-west-1', 235 | # } 236 | 237 | # Expose Prometheus monitoring metrics at the HTTP endpoint '/metrics' 238 | METRICS_ENABLED = False 239 | 240 | # Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM. 241 | NAPALM_USERNAME = "" 242 | NAPALM_PASSWORD = "" 243 | 244 | # NAPALM timeout (in seconds). (Default: 30) 245 | NAPALM_TIMEOUT = 30 246 | 247 | # NAPALM optional arguments (see https://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must 248 | # be provided as a dictionary. 249 | NAPALM_ARGS = {} 250 | 251 | # Determine how many objects to display per page within a list. (Default: 50) 252 | PAGINATE_COUNT = 50 253 | 254 | # When determining the primary IP address for a device, IPv6 is preferred over IPv4 by default. Set this to True to 255 | # prefer IPv4 instead. 256 | PREFER_IPV4 = False 257 | 258 | # Rack elevation size defaults, in pixels. For best results, the ratio of width to height should be roughly 10:1. 259 | RACK_ELEVATION_DEFAULT_UNIT_HEIGHT = 22 260 | RACK_ELEVATION_DEFAULT_UNIT_WIDTH = 220 261 | 262 | # Remote authentication support 263 | REMOTE_AUTH_ENABLED = False 264 | REMOTE_AUTH_BACKEND = "netbox.authentication.RemoteUserBackend" 265 | REMOTE_AUTH_HEADER = "HTTP_REMOTE_USER" 266 | REMOTE_AUTH_AUTO_CREATE_USER = False 267 | REMOTE_AUTH_DEFAULT_GROUPS = [] 268 | REMOTE_AUTH_DEFAULT_PERMISSIONS = {} 269 | 270 | # This repository is used to check whether there is a new release of NetBox available. Set to None to disable the 271 | # version check or use the URL below to check for release in the official NetBox repository. 272 | RELEASE_CHECK_URL = None 273 | # RELEASE_CHECK_URL = 'https://api.github.com/repos/netbox-community/netbox/releases' 274 | 275 | # The file path where custom reports will be stored. A trailing slash is not needed. Note that the default value of 276 | # this setting is derived from the installed location. 277 | # REPORTS_ROOT = '/opt/netbox/netbox/reports' 278 | 279 | # Maximum execution time for background tasks, in seconds. 280 | RQ_DEFAULT_TIMEOUT = 300 281 | 282 | # The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of 283 | # this setting is derived from the installed location. 284 | SCRIPTS_ROOT = "/config/scripts" 285 | 286 | # The name to use for the session cookie. 287 | SESSION_COOKIE_NAME = "sessionid" 288 | 289 | # By default, NetBox will store session data in the database. Alternatively, a file path can be specified here to use 290 | # local file storage instead. (This can be useful for enabling authentication on a standby instance with read-only 291 | # database access.) Note that the user as which NetBox runs must have read and write permissions to this path. 292 | SESSION_FILE_PATH = None 293 | 294 | # Time zone (default: UTC) 295 | TIME_ZONE = "UTC" 296 | 297 | # Date/time formatting. See the following link for supported formats: 298 | # https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date 299 | DATE_FORMAT = "N j, Y" 300 | SHORT_DATE_FORMAT = "Y-m-d" 301 | TIME_FORMAT = "g:i a" 302 | SHORT_TIME_FORMAT = "H:i:s" 303 | DATETIME_FORMAT = "N j, Y g:i a" 304 | SHORT_DATETIME_FORMAT = "Y-m-d H:i" 305 | -------------------------------------------------------------------------------- /src/octodns_netbox_dns/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Literal 3 | 4 | import dns.rdata 5 | import octodns.provider.base 6 | import octodns.provider.plan 7 | import octodns.record 8 | import octodns.zone 9 | import pynetbox.core.api 10 | import pynetbox.core.response 11 | 12 | 13 | class NetBoxDNSProvider(octodns.provider.base.BaseProvider): 14 | """OctoDNS provider for NetboxDNS.""" 15 | 16 | SUPPORTS_GEO = False 17 | SUPPORTS_DYNAMIC = True # pyright: ignore[reportIncompatibleMethodOverride] 18 | SUPPORTS_ROOT_NS = True 19 | SUPPORTS_MULTIVALUE_PTR = True 20 | 21 | # record types which are commented out, are not supported by the Netbox DNS plugin 22 | SUPPORTS = { # noqa: RUF012 23 | "A", 24 | "AAAA", 25 | # "ALIAS", 26 | "CAA", 27 | "CNAME", 28 | "DNAME", 29 | # "DS", 30 | "LOC", 31 | "MX", 32 | # "NAPTR", 33 | "NS", 34 | "PTR", 35 | # "SPF", 36 | "SRV", 37 | "SSHFP", 38 | # "TLSA", 39 | "TXT", 40 | # "URLFWD", 41 | } 42 | 43 | def __init__( 44 | self, 45 | id: int, # noqa: A002 46 | url: str, 47 | token: str, 48 | view: str | None | Literal[False] = None, 49 | replace_duplicates: bool = False, 50 | make_absolute: bool = False, 51 | disable_ptr: bool = True, 52 | insecure_request: bool = False, 53 | zone_status_filter: str = "active", 54 | record_status_filter: str = "active", 55 | max_page_size: int = 0, 56 | *args, 57 | **kwargs, 58 | ) -> None: 59 | """Initialize the NetBoxDNSProvider.""" 60 | self.log = logging.getLogger(f"NetBoxDNSProvider[{id}]") 61 | 62 | super().__init__(id, *args, **kwargs) 63 | 64 | self.api = pynetbox.core.api.Api(url, token) 65 | if insecure_request: 66 | import urllib3 # noqa: PLC0415 67 | 68 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 69 | self.api.http_session.verify = False 70 | self.nb_view = self._get_nb_view(view) 71 | self.replace_duplicates = replace_duplicates 72 | self.make_absolute = make_absolute 73 | self.disable_ptr = disable_ptr 74 | self.zone_status_filter = {"status": zone_status_filter} if zone_status_filter else {} 75 | self.record_status_filter = {"status": record_status_filter} if record_status_filter else {} 76 | self.max_page_size = max_page_size 77 | 78 | _init_info = {k: v for k, v in locals().items() if k not in ["self", "__class__", "token"]} 79 | self.log.debug(f"__init__: {_init_info}") 80 | 81 | def _make_absolute(self, value: str, force: bool = False) -> str: 82 | """Return dns name with trailing dot to make it absolute. 83 | 84 | @param value: dns record value 85 | @param force: when `True`, disregard configuration option `make_absolute` 86 | 87 | @return: absolute dns record value 88 | """ 89 | if value.endswith("."): 90 | return value 91 | 92 | if not (self.make_absolute or force): 93 | return value 94 | 95 | absolute_value = value + "." 96 | self.log.debug(f"relative={value}, absolute={absolute_value}") 97 | 98 | return absolute_value 99 | 100 | def _escape_semicolon(self, value: str) -> str: 101 | fixed = value.replace(";", r"\;") 102 | self.log.debug(rf"in='{value}', escaped='{fixed}'") 103 | return fixed 104 | 105 | def _unescape_semicolon(self, value: str) -> str: 106 | fixed = value.replace(r"\\", "\\").replace(r"\;", ";") 107 | self.log.debug(rf"in='{value}', unescaped='{fixed}'") 108 | return fixed 109 | 110 | def _get_nb_view(self, view: str | None | Literal[False]) -> dict[str, int]: 111 | """Get the correct netbox view. 112 | 113 | views are required since netbox-plugin-dns>=1.0.0. 114 | 115 | @param view: `None` for no view filter, else the view name 116 | 117 | @return: the netbox view id in the netbox query format 118 | """ 119 | if not view: 120 | return {} 121 | 122 | nb_view: pynetbox.core.response.Record | None = self.api.plugins.netbox_dns.views.get( 123 | name=view 124 | ) 125 | if nb_view is None: 126 | msg = f"dns view={view}, has not been found" 127 | self.log.error(msg) 128 | raise ValueError(msg) 129 | 130 | self.log.debug(f"found view={nb_view.name}, id={nb_view.id}") 131 | 132 | return {"view_id": nb_view.id} 133 | 134 | def _get_nb_zone( 135 | self, 136 | zone_name: str, 137 | ) -> pynetbox.core.response.Record: 138 | """Given a zone name and a view name, look it up in NetBox. 139 | 140 | @param name: name of the dns zone 141 | @param view: the netbox view id in the api query format 142 | 143 | @raise pynetbox.RequestError: if declared view is not existent 144 | 145 | @return: the netbox dns zone object 146 | """ 147 | nb_zone: pynetbox.core.response.Record | None = self.api.plugins.netbox_dns.zones.get( 148 | name=zone_name[:-1], **self.nb_view 149 | ) 150 | 151 | if nb_zone is None: 152 | self.log.error(f"zone={zone_name}, not found in view={self.nb_view}") 153 | raise LookupError 154 | 155 | self.log.debug(f"found zone={nb_zone.name}, id={nb_zone.id}") 156 | 157 | return nb_zone 158 | 159 | def _format_rdata(self, rcd_type: str, rcd_value: str) -> str | dict[str, Any]: 160 | """Format netbox record values to correct octodns record values. 161 | 162 | @param rcd_type: record type 163 | @param rcd_value: record value 164 | 165 | @return: formatted rrdata value 166 | """ 167 | rdata = dns.rdata.from_text("IN", rcd_type, rcd_value) 168 | match rdata.rdtype.name: 169 | case "A" | "AAAA": 170 | value = rdata.address 171 | 172 | case "CNAME" | "DNAME" | "NS" | "PTR": 173 | value = self._make_absolute(rdata.target.to_text()) 174 | 175 | case "CAA": 176 | value = { 177 | "flags": rdata.flags, 178 | "tag": rdata.tag.decode(), 179 | "value": rdata.value.decode(), 180 | } 181 | 182 | case "LOC": 183 | value = { 184 | "lat_degrees": rdata.latitude[0], 185 | "lat_minutes": rdata.latitude[1], 186 | "lat_seconds": rdata.latitude[2] + rdata.latitude[3] / 1000, 187 | "lat_direction": "N" if rdata.latitude[4] >= 0 else "S", 188 | "long_degrees": rdata.longitude[0], 189 | "long_minutes": rdata.longitude[1], 190 | "long_seconds": rdata.longitude[2] + rdata.longitude[3] / 1000, 191 | "long_direction": "W" if rdata.latitude[4] >= 0 else "E", 192 | "altitude": rdata.altitude / 100, 193 | "size": rdata.size / 100, 194 | "precision_horz": rdata.horizontal_precision / 100, 195 | "precision_vert": rdata.vertical_precision / 100, 196 | } 197 | 198 | case "MX": 199 | value = { 200 | "preference": rdata.preference, 201 | "exchange": self._make_absolute(rdata.exchange.to_text()), 202 | } 203 | 204 | case "NAPTR": 205 | value = { 206 | "order": rdata.order, 207 | "preference": rdata.preference, 208 | "flags": rdata.flags, 209 | "service": rdata.service, 210 | "regexp": rdata.regexp, 211 | "replacement": rdata.replacement.to_text(), 212 | } 213 | 214 | case "SSHFP": 215 | value = { 216 | "algorithm": rdata.algorithm, 217 | "fingerprint_type": rdata.fp_type, 218 | "fingerprint": rdata.fingerprint.hex(), 219 | } 220 | 221 | case "TXT": 222 | value = self._escape_semicolon(rcd_value) 223 | 224 | case "SRV": 225 | value = { 226 | "priority": rdata.priority, 227 | "weight": rdata.weight, 228 | "port": rdata.port, 229 | "target": self._make_absolute(rdata.target.to_text()), 230 | } 231 | 232 | case "ALIAS" | "DS" | "NAPTR" | "SPF" | "TLSA" | "URLFWD" | "SOA": 233 | self.log.debug(f"'{rcd_type}' record type not implemented. ignoring record") 234 | raise NotImplementedError 235 | 236 | case _: 237 | self.log.error(f"ignoring invalid record with type: '{rcd_type}'") 238 | raise NotImplementedError 239 | 240 | self.log.debug(rf"formatted record value={value}") 241 | 242 | return value # type: ignore[no-any-return] 243 | 244 | def _format_nb_records(self, zone: octodns.zone.Zone) -> list[dict[str, Any]]: 245 | """Format netbox dns records to the octodns format. 246 | 247 | @param zone: octodns zone 248 | 249 | @return: a list of octodns compatible record dicts 250 | """ 251 | records: dict[tuple[str, str], dict[str, Any]] = {} 252 | 253 | nb_zone = self._get_nb_zone(zone.name) 254 | nb_records: pynetbox.core.response.RecordSet = self.api.plugins.netbox_dns.records.filter( 255 | limit=self.max_page_size, 256 | zone_id=nb_zone.id, 257 | **self.record_status_filter, 258 | ) 259 | for nb_record in nb_records: 260 | rcd_name: str = "" if nb_record.name == "@" else nb_record.name 261 | rcd_value: str = ( 262 | self._make_absolute(nb_record.zone.name, True) 263 | if nb_record.value == "@" 264 | else nb_record.value 265 | ) 266 | rcd_type: str = nb_record.type 267 | rcd_ttl: int = nb_record.ttl or nb_zone.default_ttl 268 | if nb_record.type == "NS": 269 | rcd_ttl = nb_zone.soa_refresh 270 | 271 | rcd_data = { 272 | "name": rcd_name, 273 | "type": rcd_type, 274 | "ttl": rcd_ttl, 275 | "values": [], 276 | } 277 | self.log.debug(rf"working on record={rcd_data}, value={rcd_value}") 278 | 279 | try: 280 | rcd_rdata = self._format_rdata(rcd_type, rcd_value) 281 | except NotImplementedError: 282 | continue 283 | 284 | if (rcd_name, rcd_type) not in records: 285 | records[(rcd_name, rcd_type)] = rcd_data 286 | 287 | records[(rcd_name, rcd_type)]["values"].append(rcd_rdata) 288 | 289 | self.log.debug(rf"record data={records[(rcd_name, rcd_type)]}") 290 | 291 | return list(records.values()) 292 | 293 | def populate( 294 | self, zone: octodns.zone.Zone, target: bool = False, lenient: bool = False 295 | ) -> bool: 296 | """Get all the records of a zone from NetBox and add them to the OctoDNS zone. 297 | 298 | @param zone: octodns zone 299 | @param target: when `True`, load the current state of the provider. 300 | @param lenient: when `True`, skip record validation and do a "best effort" load of data. 301 | 302 | @return: true if the zone exists, else false. 303 | """ 304 | self.log.info(f"--> populate '{zone.name}', target={target}, lenient={lenient}") 305 | 306 | try: 307 | records = self._format_nb_records(zone) 308 | except LookupError: 309 | return False 310 | 311 | for data in records: 312 | if len(data["values"]) == 1: 313 | data["value"] = data.pop("values")[0] 314 | record = octodns.record.Record.new( 315 | zone=zone, 316 | name=data["name"], 317 | data=data, 318 | source=self, 319 | lenient=lenient, 320 | ) 321 | zone.add_record(record, lenient=lenient, replace=self.replace_duplicates) 322 | 323 | self.log.info(f"populate -> found {len(zone.records)} records for zone '{zone.name}'") 324 | 325 | return True 326 | 327 | def _format_changeset(self, change: Any) -> set[str]: 328 | """Format the changeset. 329 | 330 | @param change: the raw changes 331 | 332 | @return: the formatted/escaped changeset 333 | """ 334 | match change._type: 335 | case "CAA": 336 | changeset = {repr(v) for v in change.values} 337 | case "TXT": 338 | changeset = {self._unescape_semicolon(repr(v)[1:-1]) for v in change.values} 339 | case _: 340 | match change: 341 | case octodns.record.ValueMixin(): 342 | changeset = {repr(change.value)[1:-1]} 343 | case octodns.record.ValuesMixin(): 344 | changeset = {repr(v)[1:-1] for v in change.values} 345 | case _: 346 | raise ValueError 347 | 348 | self.log.debug(f"{changeset=}") 349 | 350 | return changeset 351 | 352 | # def _include_change(self, change: octodns.record.change.Change) -> bool: 353 | # """filter out record types which the provider can't create in netbox 354 | 355 | # @param change: the planned change 356 | 357 | # @return: false if the change should be discarded, true if it should be kept. 358 | # """ 359 | # return True # currently unused 360 | 361 | def _apply(self, plan: octodns.provider.plan.Plan) -> None: 362 | """Apply the changes to the NetBox DNS zone. 363 | 364 | @param plan: the planned changes 365 | 366 | @return: none 367 | """ 368 | self.log.debug(f"--> _apply zone={plan.desired.name}, changes={len(plan.changes)}") 369 | 370 | nb_zone = self._get_nb_zone(plan.desired.name) 371 | 372 | for change in plan.changes: 373 | match change: 374 | case octodns.record.Create(): 375 | rcd_name = "@" if change.new.name == "" else change.new.name 376 | 377 | new_changeset = self._format_changeset(change.new) 378 | for record in new_changeset: 379 | self.log.debug(rf"ADD {change.new._type} {rcd_name} {record}") 380 | self.api.plugins.netbox_dns.records.create( 381 | zone=nb_zone.id, 382 | name=rcd_name, 383 | type=change.new._type, 384 | ttl=change.new.ttl, 385 | value=record, 386 | disable_ptr=self.disable_ptr, 387 | ) 388 | 389 | case octodns.record.Delete(): 390 | nb_records: pynetbox.core.response.RecordSet = ( 391 | self.api.plugins.netbox_dns.records.filter( 392 | limit=self.max_page_size, 393 | zone_id=nb_zone.id, 394 | name=change.existing.name, 395 | type=change.existing._type, 396 | ) 397 | ) 398 | 399 | existing_changeset = self._format_changeset(change.existing) 400 | for nb_record in nb_records: 401 | for record in existing_changeset: 402 | if nb_record.value != record: 403 | continue 404 | self.log.debug( 405 | rf"DELETE {nb_record.type} {nb_record.name} {nb_record.value}" 406 | ) 407 | nb_record.delete() 408 | 409 | case octodns.record.Update(): 410 | rcd_name = "@" if change.existing.name == "" else change.existing.name 411 | 412 | nb_records = self.api.plugins.netbox_dns.records.filter( 413 | limit=self.max_page_size, 414 | zone_id=nb_zone.id, 415 | name=rcd_name, 416 | type=change.existing._type, 417 | ) 418 | 419 | existing_changeset = self._format_changeset(change.existing) 420 | new_changeset = self._format_changeset(change.new) 421 | 422 | to_delete = existing_changeset.difference(new_changeset) 423 | to_update = existing_changeset.intersection(new_changeset) 424 | to_create = new_changeset.difference(existing_changeset) 425 | 426 | for nb_record in nb_records: 427 | if nb_record.value in to_delete: 428 | self.log.debug( 429 | rf"DELETE {nb_record.type} {nb_record.name} {nb_record.value}" 430 | ) 431 | nb_record.delete() 432 | if nb_record.value in to_update: 433 | self.log.debug( 434 | rf"MODIFY (ttl) {nb_record.type} {nb_record.name} {nb_record.value}" 435 | ) 436 | nb_record.ttl = change.new.ttl 437 | nb_record.save() 438 | 439 | for record in to_create: 440 | self.log.debug(rf"ADD {change.new._type} {rcd_name} {record}") 441 | nb_record = self.api.plugins.netbox_dns.records.create( 442 | zone=nb_zone.id, 443 | name=rcd_name, 444 | type=change.new._type, 445 | ttl=change.new.ttl, 446 | value=record, 447 | disable_ptr=self.disable_ptr, 448 | ) 449 | 450 | def list_zones(self) -> list[str]: 451 | """Get all zones from netbox. 452 | 453 | @return: a list with all active zones 454 | """ 455 | zones = self.api.plugins.netbox_dns.zones.filter( 456 | limit=self.max_page_size, **self.nb_view, **self.zone_status_filter 457 | ) 458 | absolute_zones = [self._make_absolute(z.name, True) for z in zones] 459 | 460 | return sorted(absolute_zones) 461 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 9 | 10 | ## [v0.3.14](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.13...v0.3.14) - 2025-06-28 11 | 12 | ### Commits 13 | 14 | - update readme and view default [`3b6af9b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/3b6af9bb84e2026c8419a81096296e942b3203b9) 15 | 16 | ## [v0.3.13](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.12...v0.3.13) - 2025-06-28 17 | 18 | ### Commits 19 | 20 | - add zone_status_filter, record_status_filter & max_page_size [`ce0c5fc`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ce0c5fc0b01859341ba95fe62f381fc97c1c6e80) 21 | - update deps [`4e6402b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4e6402b2586f6419e5a0aead77294f9a079e1e88) 22 | - update deps [`b2f70b3`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/b2f70b378c690924001ee4aa401879aef5d39b26) 23 | - move SOA to not implemented records [`25a89a1`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/25a89a1f5d411635b129493f1acdf59a8ec9a61b) 24 | - update workflow [`907ff89`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/907ff89ea3fbc11ef4713652be02db61d3fe39fc) 25 | - Silently ignore SOA records [`65355e2`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/65355e239c20dd5a9f52ca709392fa130a49e68f) 26 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.3.3 [`cc47e92`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cc47e92751fb231f33274cf875224c605b45e2b6) 27 | - chore(deps): update dependency ruff to ~=0.12.1 [`984d009`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/984d009a6eae7d48c77f4e991380bf80a0188a0b) 28 | - chore(deps): update dependency coverage to ~=7.9.1 [`1117c4a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/1117c4a95666899828b2fa2fc91aa05c53792208) 29 | - chore(deps): update dependency octodns to v1.12.0 [`2dc5f2b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/2dc5f2b96042d7c994a17f721785cfb4b043d3ff) 30 | - chore(deps): update dependency octodns-spf to v1 [`af9c197`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/af9c197c5ff1ccfaa9bede201be8848da10d2252) 31 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.2.8 [`1cc6e07`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/1cc6e07f933c59709ba312ab359e2c1e095f4cf6) 32 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.2.7 [`bde9894`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/bde9894caf46cec117f4205cfb42c89964dab5ba) 33 | 34 | ## [v0.3.12](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.11...v0.3.12) - 2025-03-04 35 | 36 | ### Commits 37 | 38 | - update supported record types and add tests for all supported types [`e10a710`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e10a710f45c39b801d88b6e85fd8dd782f6a0506) 39 | - add all record types for test setup [`5f22212`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/5f22212319b633589b857b483490b5f322d3b758) 40 | - fix CAA record and deprecate SPF record type [`d8e9ed0`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d8e9ed0f108c13b7b19cc230a03b60d6a3719f78) 41 | - update README with limitations [`7247896`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/72478964ace09c3de055ffaee292a2cf16ac83b8) 42 | - fix spf test [`f70eba8`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f70eba8b5564d4f4b6c6276f301a04725f962290) 43 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.2.4 [`2532a60`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/2532a603097f0aeac04c3d7777f641b7991166cd) 44 | - remove NS record from semicolon unescape [`ec69f11`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ec69f11defc1f7b932f1e79953f5780ff17fa8ef) 45 | - fix unused parameter [`f09187b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f09187b672e564022e3c854a3e9f9049bae06445) 46 | 47 | ## [v0.3.11](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.10...v0.3.11) - 2025-02-19 48 | 49 | ### Fixed 50 | 51 | - Fix SSHFP fingerprint hexadecimal value [`#1240`](https://github.com/octodns/octodns/issues/1240) 52 | 53 | ### Commits 54 | 55 | - add new test for PTR records [`91246cc`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/91246cc53ffccc23ccb35c7c642492bda8e90290) 56 | - update sync config for tests [`dbfbb8f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/dbfbb8f8b56dcbf4f4c83665f50c5585f34705e6) 57 | - fix test for ptr record [`5f7e34c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/5f7e34c24211bdcc2e6939d9830eb5d7fe873317) 58 | - update coverage and mypy [`4b84aa4`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4b84aa4e135a5e0176a568c54016d3906f3344d1) 59 | - fix gitignore double entry [`01881e7`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/01881e7067b9273973d9599836b32924fd8d6ef8) 60 | - chore(deps): update dependency pytest to v8.3.4 [`37ce27a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/37ce27aa4875d92d612319f11f5a5d91a60be97b) 61 | - chore(deps): update dependency just to v1.39.0 [`d92731f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d92731f241590edeaca2226722cdd37898937f0f) 62 | - chore(deps): update dependency pytest to v8.3.4 [`c4a41eb`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c4a41eb2dd51aecf453e592fd82361a73f770466) 63 | - chore(deps): update dependency ruff to v0.9.6 [`f9f63a2`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f9f63a2c66b2e727f49c577ba19f0dece29cd8f4) 64 | - chore(deps): update dependency octodns to v1.11.0 [`3078dfb`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/3078dfb472990db9774a805e111b79a07f3ff863) 65 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.2.3 [`b4ece3d`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/b4ece3d2ee49a0a1c7747ee612505d69d3a686d8) 66 | - fix: Remove PTR from _include_change [`69bdc15`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/69bdc15374d2517fdf2fa2ebd5cc1e7d26b1be29) 67 | 68 | ## [v0.3.10](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.9...v0.3.10) - 2025-01-09 69 | 70 | ### Commits 71 | 72 | - fix type error on missing None [`a61e6ab`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/a61e6ab69f1ca7de4d89bda2a2b570a76d264b57) 73 | - Make CNAME '@' values absolute [`d75568d`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d75568dc8ff2a04ba4f9880a1763f098f13b024e) 74 | - allow insecure_request to netbox [`fc8e50e`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/fc8e50e32b268a8969fcc30f104f05ece2c9e6f8) 75 | - Update README.md [`4423c85`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4423c85ac189f063b73716777d82425d026bd489) 76 | - fix typo [`d941437`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d9414371cbe7cbbb568ab515ab4fc60ffb0bca13) 77 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.6 [`7ecc79d`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/7ecc79ddb567fe40d14c198742936cf8bc8d8674) 78 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.5 [`b4efc26`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/b4efc26660a6834584a5675f00728898c0d8342f) 79 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.4 [`58549b5`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/58549b52244c03e5cb775558534eeeaca6cde602) 80 | - chore(deps): update dependency octodns to v1.10.0 [`0fe6008`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/0fe600824059833e2445f1a0e042e78c87a78f38) 81 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.3 [`5c28277`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/5c28277e57a2062abe326a957d71f1816da4bdce) 82 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.2 [`4235a41`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4235a41e70bb1c46d5510c7223c30b77db9f2f53) 83 | - fix whitespace error [`024d6dc`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/024d6dc52e356c741f02892cefb4aa9af084ee25) 84 | 85 | ## [v0.3.9](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.8...v0.3.9) - 2024-09-12 86 | 87 | ### Commits 88 | 89 | - caa records: convert bytes values to string [`5a35e1b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/5a35e1b51e4ba8e9b1c9e172d11ae8684e344eb9) 90 | - fix changelog action [`015745f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/015745f353cd29861c2b6a4927e8f3cdf137f4ec) 91 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.1.0 [`d04d72b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d04d72bad043b37ac745d055f2e482493b7e962b) 92 | 93 | ## [v0.3.8](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.7...v0.3.8) - 2024-08-30 94 | 95 | ### Commits 96 | 97 | - add examples [`1283d83`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/1283d83060ca0728fe399bccbc01a1a5f27452ce) 98 | - fix cicd files [`8e168c4`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/8e168c4d88de25da1a68eff15d955968daa6b23a) 99 | - update version matrix [`00a6ff6`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/00a6ff600beeeb25e432e5ccac774c43e1fdd3fb) 100 | - set SUPPORTS_DYNAMIC to true [`cb3ea58`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cb3ea583e1ab126b49f3f31055cec8f541d78716) 101 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.0.10 [`e07b905`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e07b9054eab1e9baf7676ac36a8989a00ab23c80) 102 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.0.9 [`6b4baff`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6b4bafff5ace100d3de651c50c5d25063907819b) 103 | - chore(deps): update actions/setup-go action to v5 [`1cce3ee`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/1cce3eee7376c56ef89528e35384c75dc5a279e6) 104 | 105 | ## [v0.3.7](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.6...v0.3.7) - 2024-08-09 106 | 107 | ### Commits 108 | 109 | - update ci ciles [`6572375`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/65723753f6747921990fcd4f1b6c39c00005a92e) 110 | - always normalize zones [`f1f6602`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f1f6602985ef57d2b1531f7099a1df7515fbaa6f) 111 | - run auto-formatter [`9efd4fa`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/9efd4faea1320a8e037ef0e4a7deab89fb3a95ab) 112 | - update to netbox>4.0 and netbox-plugin-dns>1.0 [`6772f0a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6772f0a5e0a929cc589c4fb08ff989eb8a962c3c) 113 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.0.8 [`aaab7d9`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/aaab7d948e5cba8c34785b5f4c53cee3c6791184) 114 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v4.0.7 [`b528c58`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/b528c58eaed41afd59c3fa76ae5f9940d5ba5049) 115 | - remove lefthook from justfile [`f4e877e`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f4e877e2fe41dbf8a5e52bc795bf04b5da654aed) 116 | 117 | ## [v0.3.6](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.5...v0.3.6) - 2024-07-07 118 | 119 | ### Commits 120 | 121 | - update tests and deps [`adeca85`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/adeca856be84af888ec7d1de3a567fba5c439f68) 122 | - Implement Provider.list_zones for dynamic zone config support [`0051c30`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/0051c30b8d948971b7ec447595b50eef80515993) 123 | - use make_absolute function [`60b3226`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/60b322636169fd8a32804f29264fe750caff607b) 124 | - fix whitespace [`ba2a3f6`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ba2a3f6edfe2be8566c609784f659b1638c3922d) 125 | - chore(deps): update dependency octodns to v1.7.0 [`cb51054`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cb510541e25cd5b1d57afc8496b4bb89088f3b3c) 126 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v3.7.6 [`d97d2b7`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d97d2b749cd5a62e310600b31159665b44e724d7) 127 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v3.7.5 [`cdd9f0d`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cdd9f0d910f682f8ab70c66887e75f095e43094a) 128 | - chore(deps): update dependency octodns to v1.6.1 [`d70c9de`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d70c9dec0aa532c699feba8bd5991cfe7a7a13d7) 129 | - chore(deps): update lscr.io/linuxserver/netbox docker tag to v3.7.4 [`6f1424c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6f1424c25e7ce387a70094086a55ece38e6c7342) 130 | - chore(deps): update dependency octodns to v1.6.0 [`b19a598`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/b19a598c387c6cc53756bfc0b255ec6134e3aeef) 131 | - chore(deps): update dependency just to v1.25.2 [`dff84f4`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/dff84f48ab87fb257ee82ecf7ddb9e2b6993bd86) 132 | 133 | ## [v0.3.5](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.4...v0.3.5) - 2024-03-04 134 | 135 | ### Commits 136 | 137 | - add new tests [`a8ecfab`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/a8ecfab93096cd3201efcb06a65bc86a2444611a) 138 | - fix typo in method call and add function for semicolons [`afe1826`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/afe182628a7b2fc784dc48bebaf9978c85d682d2) 139 | - update ci files for github [`6b06cad`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6b06cada38751648b9365fd27b850aa2b6d250a8) 140 | - unescape the changeset before comparison to live records to have an accurate changeset. fixes changes for txt records [`4fe8be6`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4fe8be62918b5bea821f4f88a151918899b61e91) 141 | - rollback changes to semicolon escaping [`91ba03a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/91ba03af7f5f53a7f3b5218f0f4a905eebcdb914) 142 | - revert: change from repr to string in changeset [`8d29b5e`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/8d29b5e639b25bb57b43e9e85bbe78e51d70b493) 143 | - update logging [`ff1a47c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ff1a47cf8cda21695fb4e30910dd92d0f9cb8b63) 144 | - update test cases [`f38a103`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f38a1036b79b9779a8480c2d1f79132370b83bb4) 145 | - change from repr to string in changeset [`696d2a2`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/696d2a2532eef05fee95cddff627c9b09736ae52) 146 | - only unescape txt and spf records [`78448d2`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/78448d2bde85fda1718c64e6cd8983b4fcb20fe0) 147 | - fix semicolon escaping [`6e624a7`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6e624a79a75051295d1863f9af7e907d44a9b307) 148 | - fix tests [`2e468c8`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/2e468c85699808ed9ba2edb35a6cc39591869346) 149 | 150 | ## [v0.3.4](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.3...v0.3.4) - 2024-02-29 151 | 152 | ### Commits 153 | 154 | - add dev setup [`880a210`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/880a210b9bacf9158f3f8f90142c34d4e1719f45) 155 | - update dev setup [`ba9d301`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ba9d301dacf532a4fd217f6b08b4d3b407f315c6) 156 | - simplify some code [`792b9d5`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/792b9d5429c43bbf67b99b35c7852b5d1e048980) 157 | - first tests with provider support [`284f001`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/284f001236bda738693cea213cd3c1b6c5d9d16b) 158 | - revert the test changes [`09898b7`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/09898b7a228454a26bcf1e83b6cac7b556ec9aa3) 159 | - rename class and add new tests [`408cf3f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/408cf3fdd279d5911eefb21f3789410c287c6611) 160 | - add tests and move multiple replaces to regex replace [`0fb38b9`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/0fb38b9ae54efb1d593f3daa1a2af601a7e52d60) 161 | - fix some errors in record filtering [`6612dae`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6612daeee79a8759d4af3c0caa86129fce27f0b6) 162 | - rename some variables [`59a9b01`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/59a9b01fef8feadfc448716eec30e9f1157d34ae) 163 | - add first test [`3728594`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/3728594b4e022e77846468d7f8e3260ed3b13029) 164 | - fix definition [`29cf05c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/29cf05c888fb96dab1710d86168250563b4b0bf6) 165 | - add dev branch to tests [`e27f893`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e27f8938a51449718de638651550ea54a187589d) 166 | - Update dependency octodns to v1.5.0 [`6953643`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/69536438767c8c7483c330967278a19b02d99f7f) 167 | - Update lscr.io/linuxserver/netbox Docker tag to v3.7.3 [`537cd20`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/537cd2061b859e1f4bff4e009154c1ac2ff31e5f) 168 | - Update dependency octodns to v1.5.0 [`6c9f77c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6c9f77c5f420bfc06d28dbbbf5ef512fb8f32d1f) 169 | - add typehint for __init__ [`6326903`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6326903bb3990ed57045f91bae53b377677862b9) 170 | - Update lscr.io/linuxserver/netbox Docker tag to v3.7.3 [`93be71f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/93be71fabad25f1285f827e347da82a97212a425) 171 | - fix workflow name [`c5f3fa9`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c5f3fa9e00ad63231ecbd6654853510ca338883f) 172 | - fix github release action [`c81f324`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c81f3241d9841798582cbfdf75e11c5d59ea6eee) 173 | 174 | ## [v0.3.3](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.2...v0.3.3) - 2024-02-20 175 | 176 | ### Commits 177 | 178 | - fix changelog generation [`e0e7694`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e0e769409d7c5c369e7f0c0c1cbd74da1f2ddcde) 179 | 180 | ## [v0.3.2](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.1...v0.3.2) - 2024-02-20 181 | 182 | ### Commits 183 | 184 | - add issue templates, ci linting and update pre-commit [`6aeda2c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/6aeda2c48bf50c5081a2f5cf8dfed096440a6e5f) 185 | - update ci files for future releases [`86789a1`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/86789a1c4bc3ab45d34e1dfcdcc765d7890138d5) 186 | - add CONTRIBUTING.md [`74c447b`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/74c447b282949184be41f05bd975f442ac6f7b92) 187 | - add changelog [`35c069e`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/35c069efe94442c6307e3ac87185a84ad49a1282) 188 | 189 | ## [v0.3.1](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.3.0...v0.3.1) - 2024-01-25 190 | 191 | ### Commits 192 | 193 | - fix for single value rcd [`256d1a8`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/256d1a8f0ecc5de6a8d8d3e3ff10752785b3d2d2) 194 | - fix type errors [`f20e190`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f20e190ef6d4bf130550616fac7d83af721b4ff2) 195 | - fix type hint for rdata [`95c6815`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/95c6815dad6ea0b2c2eca42e83f79da08487e409) 196 | 197 | ## [v0.3.0](https://github.com/olofvndrhr/octodns-netbox-dns/compare/v0.2.1...v0.3.0) - 2024-01-25 198 | 199 | ### Commits 200 | 201 | - make populate function smaller [`c95f95f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c95f95fb8b8a2ae79b8f4db05675e98386a723b6) 202 | - add docstrings and refactor make absolute function [`f7f54f8`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f7f54f8eb71fef26c6759509a589d9afc7a91bdc) 203 | - add new workflows [`ff3dd03`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ff3dd03ce53002e457e172b8718af1e65ac7f78d) 204 | - fix mypy issues [`988502a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/988502a499d86339fc7f2f4c623c442590b1f59d) 205 | - fix deps [`de1e67c`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/de1e67c6be5063c6dbed6483f232baf9420dcfa7) 206 | 207 | ## v0.2.1 - 2024-01-09 208 | 209 | ### Fixed 210 | 211 | - remove wayward debug pprint [`#10`](https://github.com/olofvndrhr/octodns-netbox-dns/issues/10) 212 | - Record type as parameter value passed to get_nb_zone [`#7`](https://github.com/olofvndrhr/octodns-netbox-dns/issues/7) 213 | 214 | ### Commits 215 | 216 | - first commit [`cab1692`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cab1692ea57fedc54a2633330c442d2acebba1e8) 217 | - update query for views and use hatch as a build tool [`cdd0b00`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/cdd0b00cd155ac5af9d6a457fa1ecfb71081b3a9) 218 | - update to python template for future pypi releases [`4a2ba28`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4a2ba28aaa3c05fdc163a3b703c7226a74e349e6) 219 | - use fork of pynetbox until pkg_resources runtime dependency is removed [`daa03aa`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/daa03aae2df979c2469696011ce42c694192a02c) 220 | - move from provider base class to source [`5714ce5`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/5714ce5af9d738999ddb68e7b92eb60c5776cd02) 221 | - update readme and make absolute optional [`61b1b47`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/61b1b47a96bb81ab6eb216a4a0988f7682a7fa9f) 222 | - update dependencies [`4cf1c62`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4cf1c627742593a8590f26d49bd375e3b51b31bc) 223 | - _get_nb_zone() is now aware of same zones declared under different view [`c2f27f7`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c2f27f7c43f78040d79d6fdc2c84d250f6f64403) 224 | - add @ support and make cname records absolute [`9d79906`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/9d79906f960bb61aacfb464979899805bce34dbf) 225 | - implement replace duplicate option [`790a2de`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/790a2de3458e3fd9cefd0fdf35b82dfa156820bf) 226 | - fix 'Abstract base class, log property missing' [`f8251ad`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f8251ad8ffcf6f92dfcf1dfdcffe954ac55eee4c) 227 | - update view query [`3cb883a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/3cb883abd67a275e4e64a79c6a28f46d706b9679) 228 | - zone can also be configured without assigned view [`4828bbf`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/4828bbf00bb8f4946f07134a39cddc04a2ed306b) 229 | - make some records absolute [`c77cef6`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c77cef64ee5d0470ab8fa1f5fc2781e1f24a75a2) 230 | - update requirements [`66d9551`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/66d95519b1070489859d665671d50e50f94f5a8e) 231 | - Fix issue where octodns was failing on missing TTL value for NS records in PTR zones [`d3c1d0f`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d3c1d0f65390dd595441ac3dca1cf52db790ddad) 232 | - fix parameter call to better reflect function type hint. [`e4db78a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e4db78a281a27765271f1afda07dee08602ed2d0) 233 | - Use upstream pynetbox library as their pull req #426 is merged. [`423207e`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/423207e5dd1673f306fb392cc5d37f63a4545fe5) 234 | - fix attribute error in case view is null [`693ea86`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/693ea866296132b1cd94e2dda61ae94e94a77557) 235 | - fix log merge [`f0b502a`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/f0b502a74f3ea72cfd78d9837bbbcc0934e0b826) 236 | - fix literal type [`bec8384`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/bec83845577417d0ede5bd57a7772946bd997142) 237 | - update debug log [`3537917`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/353791797ded363c9610b5f40e4bd857b93fd328) 238 | - escape ; character in txt records [`ac395e8`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/ac395e84e24b1b271ff76721137e2d82f548553c) 239 | - fix request parameter [`d4c0955`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d4c095540f0b925dcca249d2896ca890731af256) 240 | - add log message for total records found [`14a98fe`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/14a98fe132b3e6789ca3ad410bc101bac00b4f78) 241 | - view argument type hints must also accept None value [`c5ae5bf`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/c5ae5bf4057756030f0447681e9e6c2dccb59ec1) 242 | - update lock [`d4a4032`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/d4a403261fb2e80a4335071cc17dbff00fa9366c) 243 | - Fix invalid query to Netbox, search only with zone ID. [`e752d3d`](https://github.com/olofvndrhr/octodns-netbox-dns/commit/e752d3db221f9b79e3df5dc30ca40f597a655873) 244 | --------------------------------------------------------------------------------