├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── cd.yml │ ├── check_links.yml │ ├── ci.yml │ ├── gorelease.yml │ ├── osv-scanner.yml │ ├── replace_version.yml │ ├── semantic_tag.yml │ ├── spellcheck.yml │ └── stale.yml ├── .gitignore ├── .gitleaks.toml ├── .golangci.yml ├── .goreleaser.yaml ├── .pre-commit-config.yaml ├── .releaserc ├── .trivyignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── README.md └── init_test.go ├── cmd └── netbox-ssot │ └── main.go ├── cspell.json ├── go.mod ├── go.sum ├── internal ├── constants │ ├── constants.go │ └── symbols.go ├── logger │ ├── logger.go │ └── logger_test.go ├── netbox │ ├── inventory │ │ ├── add_items.go │ │ ├── add_items_test.go │ │ ├── add_predefined_items.go │ │ ├── add_predefined_items_test.go │ │ ├── delete_items.go │ │ ├── delete_items_test.go │ │ ├── get_items.go │ │ ├── get_items_test.go │ │ ├── helpers.go │ │ ├── init_items.go │ │ ├── init_items_test.go │ │ ├── inventory.go │ │ ├── inventory_test.go │ │ ├── orphan_manager.go │ │ ├── orphan_manager_test.go │ │ └── test_objects.go │ ├── mapper │ │ └── mapper.go │ ├── objects │ │ ├── common.go │ │ ├── common_test.go │ │ ├── dcim.go │ │ ├── dcim_test.go │ │ ├── extras.go │ │ ├── extras_test.go │ │ ├── generic_objects.go │ │ ├── ipam.go │ │ ├── ipam_test.go │ │ ├── tenancy.go │ │ ├── tenancy_test.go │ │ ├── virtualization.go │ │ ├── virtualization_test.go │ │ ├── wireless.go │ │ └── wireless_test.go │ └── service │ │ ├── client.go │ │ ├── client_test.go │ │ ├── rest.go │ │ ├── rest_test.go │ │ └── test_objects.go ├── parser │ ├── parser.go │ └── parser_test.go ├── source │ ├── common │ │ ├── common.go │ │ ├── common_test.go │ │ └── utils.go │ ├── dnac │ │ ├── dnac.go │ │ ├── dnac_init.go │ │ ├── dnac_sync.go │ │ └── dnac_test.go │ ├── fmc │ │ ├── client │ │ │ ├── api.go │ │ │ ├── client.go │ │ │ └── fmc_structs.go │ │ ├── fmc.go │ │ ├── fmc_init.go │ │ ├── fmc_sync.go │ │ └── fmc_test.go │ ├── fortigate │ │ ├── fortigate.go │ │ ├── fortigate_init.go │ │ ├── fortigate_sync.go │ │ └── fortigate_test.go │ ├── ios-xe │ │ ├── iosxe.go │ │ ├── iosxe_filters.go │ │ ├── iosxe_init.go │ │ ├── iosxe_schemas.go │ │ ├── iosxe_sync.go │ │ └── iosxe_test.go │ ├── ovirt │ │ ├── ovirt.go │ │ ├── ovirt_init.go │ │ ├── ovirt_sync.go │ │ └── ovirt_test.go │ ├── paloalto │ │ ├── paloalto.go │ │ ├── paloalto_init.go │ │ ├── paloalto_sync.go │ │ └── paloalto_test.go │ ├── proxmox │ │ ├── proxmox.go │ │ ├── proxmox_init.go │ │ ├── proxmox_sync.go │ │ └── proxmox_test.go │ ├── source.go │ ├── source_test.go │ └── vmware │ │ ├── vmware.go │ │ ├── vmware_init.go │ │ ├── vmware_sync.go │ │ └── vmware_test.go └── utils │ ├── dcim.go │ ├── dcim_test.go │ ├── diff_map.go │ ├── diff_map_test.go │ ├── http.go │ ├── http_test.go │ ├── json.go │ ├── json_test.go │ ├── netbox_marshal.go │ ├── netbox_marshal_test.go │ ├── networking.go │ ├── networking_test.go │ ├── utils.go │ └── utils_test.go ├── k8s ├── cronjob.yaml └── cronjob_with_cert.yaml ├── project-words.txt ├── renovate.json └── testdata ├── certificate ├── cert.pem └── invalid_cert.pem └── parser ├── invalid_config1.yaml ├── invalid_config10.yaml ├── invalid_config11.yaml ├── invalid_config12.yaml ├── invalid_config13.yaml ├── invalid_config14.yaml ├── invalid_config15.yaml ├── invalid_config16.yaml ├── invalid_config17.yaml ├── invalid_config18.yaml ├── invalid_config19.yaml ├── invalid_config2.yaml ├── invalid_config20.yaml ├── invalid_config21.yaml ├── invalid_config22.yaml ├── invalid_config23.yaml ├── invalid_config24.yaml ├── invalid_config25.yaml ├── invalid_config26.yaml ├── invalid_config27.yaml ├── invalid_config28.yaml ├── invalid_config29.yaml ├── invalid_config3.yaml ├── invalid_config30.yaml ├── invalid_config31.yaml ├── invalid_config32.yaml ├── invalid_config33.yaml ├── invalid_config34.yaml ├── invalid_config35.yaml ├── invalid_config36.yaml ├── invalid_config37.yaml ├── invalid_config38.yaml ├── invalid_config39.yaml ├── invalid_config4.yaml ├── invalid_config40.yaml ├── invalid_config41.yaml ├── invalid_config42.yaml ├── invalid_config43.yaml ├── invalid_config44.yaml ├── invalid_config45.yaml ├── invalid_config46.yaml ├── invalid_config47.yaml ├── invalid_config48.yaml ├── invalid_config5.yaml ├── invalid_config6.yaml ├── invalid_config7.yaml ├── invalid_config8.yaml ├── invalid_config9.yaml ├── valid_config1.yaml ├── valid_config2.yaml ├── valid_config3.yaml ├── valid_config4.yaml ├── valid_config5.yaml ├── valid_config6.yaml └── valid_config7.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | config.yaml 2 | config.example.yaml 3 | .vscode 4 | .gitignore 5 | .git 6 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug or issue with netbox-ssot 3 | title: "[Bug] " 4 | labels: ["Bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for reporting a bug! 🙏 Please take a few moments to fill out the details below. 10 | 11 | ⚠️ **Important Notes**: 12 | - Issues missing critical details may be closed without explanation. 13 | - Be sure to provide the requested **configuration**, **logs**, and other details. This helps us diagnose and resolve your issue faster. 14 | 15 | - type: dropdown 16 | id: urgent 17 | attributes: 18 | label: Is this urgent? 19 | description: Is this issue blocking critical functionality? 20 | options: 21 | - "No" 22 | - "Yes, it's critical" 23 | - "Yes, but it's not blocking" 24 | validations: 25 | required: true 26 | 27 | - type: dropdown 28 | id: method-of-running 29 | attributes: 30 | label: How are you running netbox-ssot? 31 | description: Select the method you are using to run netbox-ssot. 32 | options: 33 | - Docker 34 | - Podman 35 | - Kubernetes (K8s) 36 | - From source (Go) 37 | - Other 38 | validations: 39 | required: true 40 | 41 | - type: input 42 | id: version 43 | attributes: 44 | label: Which version of netbox are you using 45 | description: | 46 | You can find version of your netbox instance in the right end of the footer of the web interface. 47 | validations: 48 | required: true 49 | 50 | - type: input 51 | id: ssot-version 52 | attributes: 53 | label: Which version of netbox-ssot are you running 54 | description: | 55 | Copy paste the version line at the top of your logs. 56 | It MUST be in the form `Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`. 57 | validations: 58 | required: true 59 | 60 | - type: dropdown 61 | id: module-problem 62 | attributes: 63 | label: Which module has the issue? 64 | description: Indicate the module where the issue occurs. 65 | options: 66 | - General (not module-specific) 67 | - Parser 68 | - Inventory 69 | - Ovirt 70 | - VMware 71 | - DNAC 72 | - Proxmox 73 | - Palo Alto 74 | - Fortigate 75 | - FMC 76 | - IOS-XE 77 | - Other 78 | validations: 79 | required: true 80 | 81 | 82 | - type: textarea 83 | id: config 84 | attributes: 85 | label: Share your configuration 86 | description: Provide your `configuration.yml` (excluding sensitive information) used with netbox-ssot. 87 | render: yaml 88 | validations: 89 | required: true 90 | 91 | - type: textarea 92 | id: problem 93 | attributes: 94 | label: What is the problem? 95 | placeholder: "Describe the issue you are experiencing." 96 | description: Include as much detail as possible about the bug. 97 | validations: 98 | required: true 99 | 100 | - type: textarea 101 | id: steps 102 | attributes: 103 | label: Steps to reproduce 104 | description: | 105 | Outline the exact steps needed to reproduce the issue, e.g.: 106 | 1. Go to '...' 107 | 2. Run '...' 108 | 3. Observe the error. 109 | placeholder: "1. Step one...\n2. Step two...\n3. Error occurs..." 110 | validations: 111 | required: true 112 | 113 | - type: textarea 114 | id: expected 115 | attributes: 116 | label: What did you expect to happen? 117 | placeholder: "e.g., The feature should work as described in the documentation." 118 | validations: 119 | required: true 120 | 121 | - type: textarea 122 | id: actual 123 | attributes: 124 | label: What actually happened? 125 | placeholder: "e.g., Instead of the expected behavior, the following error occurred..." 126 | validations: 127 | required: true 128 | 129 | - type: textarea 130 | id: logs 131 | attributes: 132 | label: Share DEBUG-level logs (remove sensitive information) 133 | description: Attach DEBUG-level logs for the session when the issue occurred. Make sure to redact sensitive data. 134 | render: plaintext 135 | 136 | - type: textarea 137 | id: environment 138 | attributes: 139 | label: Share your environment details 140 | description: Include additional information about your environment, such as OS, architecture, or dependencies. 141 | placeholder: "e.g., Ubuntu 22.04, x86_64, Docker 20.10.8" 142 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Disable blank issue creation 2 | blank_issues_enabled: false 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or enhancement for netbox-ssot 3 | title: "[Feature Request] <title>" 4 | labels: ["Feature Request"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for suggesting a feature! 🎉 Your input helps us improve Netbox-ssot. 10 | 11 | Please provide as much detail as possible to help us understand your idea and its use case. 12 | 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: What is the feature? 17 | description: | 18 | Provide a clear and concise description of the feature you'd like to see. Include details about what it does and why it's needed. 19 | placeholder: "I would like Netbox-ssot to have the ability to..." 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: use-case 25 | attributes: 26 | label: Why is this feature important? 27 | description: | 28 | Explain the problem this feature would solve or the benefits it would provide. Include specific scenarios where this feature would be useful. 29 | placeholder: "This feature would help me by..." 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: implementation-ideas 35 | attributes: 36 | label: How could this feature be implemented? 37 | description: | 38 | Share your ideas for how this feature might work or be implemented. If you're unsure, feel free to leave this blank. 39 | placeholder: | 40 | - This feature could be added as a configuration option... 41 | - A similar tool does it this way... 42 | render: plaintext 43 | 44 | - type: textarea 45 | id: alternatives 46 | attributes: 47 | label: Have you considered alternatives? 48 | description: | 49 | List any alternative solutions or workarounds you've tried or thought about. 50 | placeholder: "Instead of this feature, I tried..." 51 | render: plaintext 52 | 53 | - type: textarea 54 | id: extra 55 | attributes: 56 | label: Additional context or references 57 | description: | 58 | Provide links, screenshots, or other references that help illustrate your feature request. 59 | placeholder: | 60 | - Here’s an example of the desired behavior: [link](https://example.com) 61 | - Screenshot of the issue: ![screenshot](url) 62 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Deploy image to ghcr.io 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v* 8 | paths: 9 | - cmd/** 10 | - internal/** 11 | - pkg/** 12 | - Dockerfile 13 | - .dockerignore 14 | - .golangci.yml 15 | - go.mod 16 | - go.sum 17 | 18 | jobs: 19 | build_and_push: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Login to ghcr.io 23 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 24 | with: 25 | registry: ghcr.io 26 | username: src-csm 27 | password: ${{ secrets.GHCR_PAT }} 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3 34 | 35 | - name: Set created date for image 36 | id: build_date 37 | run: echo "CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT" 38 | 39 | - name: Build and push final image 40 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0 41 | with: 42 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v6,linux/arm/v7 43 | tags: | 44 | ghcr.io/src-doo/netbox-ssot:latest 45 | ghcr.io/src-doo/netbox-ssot:${{ github.ref_name }} 46 | push: true 47 | build-args: | 48 | VERSION=${{ github.ref_name }} 49 | CREATED=${{ steps.build_date.outputs.CREATED }} 50 | COMMIT=${{ github.sha }} 51 | -------------------------------------------------------------------------------- /.github/workflows/check_links.yml: -------------------------------------------------------------------------------- 1 | name: Check for empty links 2 | 3 | on: 4 | repository_dispatch: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: "00 18 * * *" 8 | 9 | jobs: 10 | linkChecker: 11 | permissions: 12 | issues: write # required for peter-evans/create-issue-from-file 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | 17 | - name: Link Checker 18 | id: lychee 19 | uses: lycheeverse/lychee-action@f613c4a64e50d792e0b31ec34bbcbba12263c6a6 # v2.3.0 20 | with: 21 | fail: false 22 | 23 | - name: Create Issue From File 24 | if: steps.lychee.outputs.exit_code != 0 25 | uses: peter-evans/create-issue-from-file@e8ef132d6df98ed982188e460ebb3b5d4ef3a9cd # v5 26 | with: 27 | title: Link Checker Report 28 | content-filepath: ./lychee/out.md 29 | labels: report, automated issue 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continious integration 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | tests: 10 | name: Run tests and upload results 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: ["1.22", "1.23"] 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 19 | 20 | # This is currently workaround for checking if gofiles have changed, 21 | # Because paths filter doesn't work with required checks 22 | - name: Get changed files 23 | id: changed-files 24 | uses: tj-actions/changed-files@dcc7a0cba800f454d79fff4b993e8c3555bcc0a8 # v45 25 | with: 26 | files: | 27 | cmd/** 28 | internal/** 29 | .golangci.yml 30 | go.mod 31 | go.sum 32 | 33 | - name: Setup Go 34 | if: steps.changed-files.outputs.any_modified == 'true' 35 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 36 | with: 37 | go-version: ${{ matrix.go-version }} 38 | 39 | - name: golangci-lint 40 | if: steps.changed-files.outputs.any_modified == 'true' 41 | uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6 42 | with: 43 | version: v1.61.0 44 | args: --timeout=5m 45 | 46 | - name: Install dependencies 47 | if: steps.changed-files.outputs.any_modified == 'true' 48 | run: go mod download 49 | 50 | - name: Test with Go 51 | if: steps.changed-files.outputs.any_modified == 'true' 52 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 53 | 54 | - name: Upload coverage reports to Codecov 55 | if: steps.changed-files.outputs.any_modified == 'true' 56 | uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 57 | with: 58 | token: ${{ secrets.CODECOV_TOKEN }} 59 | files: cover.txt 60 | slug: src-doo/netbox-ssot 61 | 62 | vulnerabilities: 63 | name: Check for vulnerabilities 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Checkout code 67 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 68 | 69 | - name: Get changed files 70 | id: changed-files 71 | uses: tj-actions/changed-files@dcc7a0cba800f454d79fff4b993e8c3555bcc0a8 # v45 72 | with: 73 | files: | 74 | cmd/** 75 | internal/** 76 | .golangci.yml 77 | go.mod 78 | go.sum 79 | .dockerignore 80 | Dockerfile 81 | 82 | # https://github.com/aquasecurity/trivy-action?tab=readme-ov-file#scan-ci-pipeline 83 | - name: Build an image from Dockerfile 84 | if: steps.changed-files.outputs.any_modified == 'true' 85 | run: | 86 | docker build -t netbox-ssot:${{ github.sha }} . 87 | 88 | - name: Run Trivy vulnerability scanner 89 | if: steps.changed-files.outputs.any_modified == 'true' 90 | uses: aquasecurity/trivy-action@master 91 | # We use proxies to avoid rate limiting for trivy database 92 | env: 93 | TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db 94 | TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db 95 | with: 96 | image-ref: netbox-ssot:${{ github.sha }} 97 | format: table 98 | exit-code: '1' 99 | ignore-unfixed: true 100 | vuln-type: 'os,library' 101 | severity: 'CRITICAL,HIGH' 102 | -------------------------------------------------------------------------------- /.github/workflows/gorelease.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - v\d+\.\d+\.\d+ 7 | workflow_dispatch: 8 | 9 | jobs: 10 | goreleaser: 11 | permissions: 12 | contents: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5 22 | 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6 25 | with: 26 | distribution: goreleaser 27 | version: '~> v2' 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/osv-scanner.yml: -------------------------------------------------------------------------------- 1 | name: OSV-Scanner PR Scan 2 | 3 | # Change "main" to your default branch if you use a different name, i.e. "master" 4 | on: 5 | pull_request: 6 | branches: [main] 7 | merge_group: 8 | branches: [main] 9 | 10 | permissions: 11 | # Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117 12 | actions: read 13 | # Require writing security events to upload SARIF file to security tab 14 | security-events: write 15 | # Only need to read contents 16 | contents: read 17 | 18 | jobs: 19 | scan-pr: 20 | uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.9.1" 21 | with: 22 | scan-args: |- 23 | -r 24 | --skip-git 25 | ./ 26 | -------------------------------------------------------------------------------- /.github/workflows/replace_version.yml: -------------------------------------------------------------------------------- 1 | name: Replace version in k8s manifests on main tag push 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | jobs: 8 | replace_version: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | with: 14 | token: ${{ secrets.SVC_PAT }} 15 | ref: main 16 | 17 | - name: Replace image version in k8s manifests 18 | run: | 19 | for file in k8s/*.yaml; do 20 | sed -i "s|ghcr.io/src-doo/netbox-ssot:v.*.*.*|ghcr.io/src-doo/netbox-ssot:${{ github.ref_name }}|g" "$file" 21 | done 22 | 23 | - name: Commit and push changes 24 | run: | 25 | git config --global user.name "src-csm" 26 | git config --global user.email "199741225+src-csm@users.noreply.github.com" 27 | git add . 28 | git commit -m "chore(k8s): Replace version in k8s manifests" 29 | git push -f 30 | -------------------------------------------------------------------------------- /.github/workflows/semantic_tag.yml: -------------------------------------------------------------------------------- 1 | name: Create semantic tag 2 | on: 3 | push: 4 | branches: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4 18 | with: 19 | node-version: "lts/*" 20 | 21 | - name: Release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.SVC_PAT }} 24 | run: npx semantic-release@24.2.3 25 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Check spelling with spellcheck 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | spellcheck: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 11 | - uses: streetsidesoftware/cspell-action@ef95dc49d631fc2a9e9ea089ae2b2127b7c4588e # v6 12 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write # only for delete-branch option 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 15 | with: 16 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.' 17 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 18 | stale-issue-label: 'no-issue-activity' 19 | exempt-issue-labels: 'awaiting-approval,work-in-progress,discussion,help wanted' 20 | 21 | stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 22 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 23 | stale-pr-label: 'no-pr-activity' 24 | exempt-pr-labels: 'awaiting-approval,work-in-progress' 25 | 26 | days-before-issue-stale: 60 27 | days-before-pr-stale: 60 28 | days-before-issue-close: 7 29 | days-before-pr-close: 10 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | config.yaml 4 | __debug* 5 | sub.pem 6 | main 7 | 8 | # Created by https://www.toptal.com/developers/gitignore/api/go 9 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 10 | 11 | ### Go ### 12 | # If you prefer the allow list template instead of the deny list, see community template: 13 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 14 | # 15 | # Binaries for programs and plugins 16 | *.exe 17 | *.exe~ 18 | *.dll 19 | *.so 20 | *.dylib 21 | 22 | # Test binary, built with `go test -c` 23 | *.test 24 | 25 | # Output of the go coverage tool, specifically when used with LiteIDE 26 | *.out 27 | 28 | # Dependency directories (remove the comment below to include it) 29 | # vendor/ 30 | 31 | # Go workspace file 32 | go.work 33 | 34 | # Ignore MacOS files 35 | .DS_Store 36 | 37 | # Ignore goreleaser dir 38 | dist/ 39 | 40 | # End of https://www.toptal.com/developers/gitignore/api/go 41 | 42 | .pre-commit-trivy-cache 43 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | title = "gitleaks config" 2 | 3 | # https://github.com/gitleaks/gitleaks?tab=readme-ov-file#configuration 4 | [allowlist] 5 | description = "global allow lists" 6 | paths = [ 7 | '''internal/parser/testdata/*''', 8 | ] -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | lll: 3 | line-length: 120 4 | misspell: 5 | locale: US 6 | errcheck: 7 | exclude-functions: 8 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Debug 9 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Debugf 10 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Info 11 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Infof 12 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Warning 13 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Warningf 14 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Error 15 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Errorf 16 | - (*github.com/src-doo/netbox-ssot/internal/logger.Logger).Output 17 | 18 | issues: 19 | max-issues-per-linter: 50 20 | max-same-issues: 20 21 | exclude: 22 | - G402 23 | 24 | linters: 25 | enable: 26 | - revive 27 | - gocyclo 28 | - lll 29 | # - dupl # TODO: enable in future 30 | # - errorlint # TODO: enable in future 31 | # - gocognit # TODO: enable in future 32 | # - cyclop # TODO: enable in future 33 | # - paralleltest # TODO: enable in future 34 | # - nestif # TODO: enable in future 35 | # - maintidx # TODO: enable in future 36 | # - exhaustive # TODO: enable in future 37 | # - nilnil # TODO: enable in future 38 | # - gochecknoglobals # TODO: enable in future 39 | # - goerr113 # TODO: enable in future 40 | # - ireturn # TODO: enable in future 41 | # - containedctx 42 | - gosec 43 | - gocritic 44 | - asasalint 45 | - asciicheck 46 | - bidichk 47 | - bodyclose 48 | - decorder 49 | - dogsled 50 | - dupword 51 | - durationcheck 52 | - errchkjson 53 | - errname 54 | - forcetypeassert 55 | - gci 56 | - gocheckcompilerdirectives 57 | - gochecknoinits 58 | - goconst 59 | - godot 60 | - goheader 61 | - goimports 62 | - mnd 63 | - copyloopvar 64 | - gomoddirectives 65 | - goprintffuncname 66 | - gosmopolitan 67 | - grouper 68 | - importas 69 | - interfacebloat 70 | - makezero 71 | - mirror 72 | - misspell 73 | - musttag 74 | - nakedret 75 | - nilerr 76 | - noctx 77 | - nolintlint 78 | - nosprintfhostport 79 | - prealloc 80 | - predeclared 81 | - promlinter 82 | - reassign 83 | - rowserrcheck 84 | - sqlclosecheck 85 | - tagalign 86 | - tenv 87 | - thelper 88 | - tparallel 89 | - unconvert 90 | - unparam 91 | - usestdlibvars 92 | - wastedassign 93 | - whitespace 94 | - zerologlint 95 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | project_name: netbox-ssot 5 | 6 | env: 7 | - GO111MODULE=on 8 | 9 | before: 10 | hooks: 11 | - go mod download 12 | 13 | builds: 14 | - id: dynamic 15 | binary: netbox-ssot 16 | main: ./cmd/netbox-ssot/main.go 17 | env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | - windows 22 | - darwin 23 | goarch: 24 | - amd64 25 | - "386" 26 | - arm64 27 | goarm: 28 | - "6" 29 | - "7" 30 | 31 | archives: 32 | - formats: 33 | - tar.gz 34 | # this name template makes the OS and Arch compatible with the results of `uname`. 35 | name_template: >- 36 | {{ .ProjectName }}_ 37 | {{- title .Os }}_ 38 | {{- if eq .Arch "amd64" }}x86_64 39 | {{- else if eq .Arch "386" }}i386 40 | {{- else }}{{ .Arch }}{{ end }} 41 | {{- if .Arm }}v{{ .Arm }}{{ end }} 42 | # use zip for windows archives 43 | format_overrides: 44 | - goos: windows 45 | formats: 46 | - zip 47 | 48 | changelog: 49 | sort: asc 50 | filters: 51 | exclude: 52 | - "^docs:" 53 | - "^test:" 54 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-json 7 | - id: check-xml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - id: pretty-format-json 11 | - id: check-case-conflict 12 | 13 | - repo: https://github.com/golangci/golangci-lint 14 | rev: v1.64.5 15 | hooks: 16 | - id: golangci-lint 17 | 18 | - repo: https://github.com/segmentio/golines 19 | rev: v0.12.2 20 | hooks: 21 | - id: golines 22 | name: golines 23 | description: A golang formatter that fixes long lines. 24 | entry: golines -w ./internal 25 | pass_filenames: true 26 | types: [go] 27 | language: golang 28 | 29 | - repo: https://github.com/dnephin/pre-commit-golang 30 | rev: v0.5.1 31 | hooks: 32 | - id: go-unit-tests 33 | - id: go-mod-tidy 34 | 35 | - repo: https://github.com/gitleaks/gitleaks 36 | rev: v8.23.3 37 | hooks: 38 | - id: gitleaks 39 | 40 | - repo: https://github.com/streetsidesoftware/cspell-cli 41 | rev: v8.17.2 42 | hooks: 43 | - id: cspell 44 | 45 | - repo: https://github.com/google/osv-scanner/ 46 | rev: v2.0.0-beta2 47 | hooks: 48 | - id: osv-scanner 49 | args: ["-r", "."] 50 | 51 | - repo: https://github.com/mxab/pre-commit-trivy.git 52 | rev: v0.14.0 53 | hooks: 54 | - id: trivyfs-docker 55 | args: 56 | - --skip-dirs 57 | - ./tests 58 | - . # last arg indicates the path/file to scan 59 | - id: trivyconfig-docker 60 | args: 61 | - --skip-dirs 62 | - ./tests 63 | - . # last arg indicates the path/file to scan 64 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "plugins": [ 6 | "@semantic-release/commit-analyzer", 7 | "@semantic-release/release-notes-generator" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.trivyignore: -------------------------------------------------------------------------------- 1 | DS026 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.24.0@sha256:2b1cbf278ce05a2a310a3d695ebb176420117a8cfcfcc4e5e68a1bef5f6354da AS builder 2 | 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG VERSION 6 | ARG CREATED 7 | ARG COMMIT 8 | 9 | WORKDIR /app 10 | 11 | COPY go.mod go.sum ./ 12 | 13 | RUN go mod download 14 | 15 | COPY ./internal ./internal 16 | 17 | COPY ./cmd ./cmd 18 | 19 | RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ 20 | go build -trimpath -ldflags="-s -w \ 21 | -X 'main.version=$VERSION' \ 22 | -X 'main.commit=$COMMIT' \ 23 | -X 'main.date=$CREATED' \ 24 | " -o ./cmd/netbox-ssot/main ./cmd/netbox-ssot/main.go 25 | 26 | FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c 27 | 28 | ARG VERSION 29 | ARG CREATED 30 | ARG COMMIT 31 | 32 | LABEL \ 33 | org.opencontainers.image.authors="src-doo" \ 34 | org.opencontainers.image.created=$CREATED \ 35 | org.opencontainers.image.version=$VERSION \ 36 | org.opencontainers.image.revision=$COMMIT \ 37 | org.opencontainers.image.url="https://github.com/src-doo/netbox-ssot" \ 38 | org.opencontainers.image.documentation="https://github.com/src-doo/netbox-ssot/blob/main/README.md" \ 39 | org.opencontainers.image.source="https://github.com/src-doo/netbox-ssot" \ 40 | org.opencontainers.image.title="Netbox-ssot" \ 41 | org.opencontainers.image.description="Microservice for syncing Netbox with multiple external sources." 42 | 43 | # Install openssh required for netconf 44 | RUN apk add --no-cache openssh 45 | 46 | # Create a netbox user and group 47 | RUN addgroup -S -g 10001 netbox && \ 48 | adduser -S -u 10001 -G netbox netbox && \ 49 | mkdir -p /app && \ 50 | chown -R netbox:netbox /app 51 | USER netbox:netbox 52 | 53 | # Also allow deprecated ssh algorithims for older devices 54 | # See https://github.com/SRC-doo/netbox-ssot/issues/498 55 | RUN mkdir -p /home/netbox/.ssh/ && \ 56 | cat <<EOF > /home/netbox/.ssh/config 57 | Host * 58 | HostKeyAlgorithms +ssh-rsa 59 | PubkeyAcceptedKeyTypes +ssh-rsa 60 | EOF 61 | 62 | WORKDIR /app 63 | 64 | COPY --from=builder --chown=netbox:netbox /app/cmd/netbox-ssot/main ./main 65 | 66 | CMD ["./main"] 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gasper Oblak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: build_and_push 4 | 5 | build_and_push: 6 | docker buildx build \ 7 | --platform linux/amd64,linux/arm64,linux/arm/v7 \ 8 | -t ghcr.io/src-doo/netbox-ssot:develop --push . 9 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | ## Resource usage 4 | 5 | ### Netbox 6 | 7 | Tested on netbox deployment, with netbox using the following resource limits: 8 | 9 | | CPU req | CPU limit | MEM req | MEM limit | 10 | | ------- | --------- | ------- | --------- | 11 | | 500m | 2 | 128Mi | 2Gi | 12 | 13 | Netbox resource usage (grafana) 14 | - single 15 | 16 | | MEM (single) | MEM (parallel) | CPU (single) | CPU (parallel) | 17 | | ------------ | -------------- | ------------ | -------------- | 18 | | 750MiB | | 0.355 | | 19 | 20 | - multiple 21 | 22 | ### Netbox-ssot 23 | 24 | Tested on netbox deployment, with netbox using the following resource limits: 25 | 26 | | CPU req | CPU limit | MEM req | MEM limit | 27 | | ------- | --------- | ------- | --------- | 28 | | 50m | 100m | 50Mi | 100Mi | 29 | 30 | Netbox resource usage (grafana) 31 | 32 | | MEM (single) | MEM (parallel) | CPU (single) | CPU(parallel) | 33 | | ------------ | -------------- | ------------ | ------------- | 34 | | 40MiB | | 0.004 | | 35 | 36 | 37 | ## Init Run 38 | 39 | - `v0.1.5` init run vs parallel run 40 | - around 6000 objects total 41 | - 4 external sources 42 | 43 | | Single goroutine | Parallel | 44 | | ---------------- | -------- | 45 | | 24m 30s | | 46 | 47 | ## Sync Run 48 | 49 | - `v0.1.5` sync run (around 6000 objects total) 50 | - around 5k objects total 51 | - 4 external sources 52 | 53 | | Single goroutine | Parallel | 54 | | ---------------- | -------- | 55 | | 2m 27s | | 56 | | 2m 17s | | 57 | | 2m 19s | | 58 | -------------------------------------------------------------------------------- /benchmark/init_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/src-doo/netbox-ssot/internal/constants" 8 | "github.com/src-doo/netbox-ssot/internal/logger" 9 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 11 | "github.com/src-doo/netbox-ssot/internal/parser" 12 | "github.com/src-doo/netbox-ssot/internal/utils" 13 | ) 14 | 15 | const ( 16 | NumberOfSites = 10 17 | NumberOfManufacturers = 100 18 | NumberOfPlatforms = 1000 19 | NumberOfContacts = 1000 20 | ) 21 | 22 | func main() { 23 | config, err := parser.ParseConfig("config.yaml") 24 | if err != nil { 25 | fmt.Println("Parser:", err) 26 | return 27 | } 28 | benchmarkCtx := context.WithValue(context.Background(), constants.CtxSourceKey, "benchmark") 29 | // Initialize Logger 30 | mainLogger, err := logger.New(config.Logger.Dest, config.Logger.Level) 31 | if err != nil { 32 | fmt.Println("Logger:", err) 33 | return 34 | } 35 | inventoryLogger, err := logger.New(config.Logger.Dest, config.Logger.Level) 36 | if err != nil { 37 | mainLogger.Errorf(benchmarkCtx, "inventoryLogger: %s", err) 38 | } 39 | nbi := inventory.NewNetboxInventory(benchmarkCtx, inventoryLogger, config.Netbox) 40 | mainLogger.Debug(benchmarkCtx, "Netbox inventory: ", nbi) 41 | 42 | err = nbi.Init() 43 | if err != nil { 44 | mainLogger.Error(benchmarkCtx, err) 45 | return 46 | } 47 | initSites(benchmarkCtx, NumberOfSites, nbi) 48 | InitManufacturers(benchmarkCtx, NumberOfManufacturers, nbi) 49 | InitPlatforms(benchmarkCtx, NumberOfPlatforms, nbi) 50 | initContacts(benchmarkCtx, NumberOfContacts, nbi) 51 | } 52 | 53 | func initSites(ctx context.Context, n int, nbi *inventory.NetboxInventory) { 54 | for i := 0; i < n; i++ { 55 | siteName := fmt.Sprintf("Site %d", i) 56 | _, err := nbi.AddSite(ctx, &objects.Site{ 57 | Name: siteName, 58 | Slug: utils.Slugify(siteName), 59 | }) 60 | if err != nil { 61 | fmt.Printf("Adding site: %s", err) 62 | } 63 | } 64 | } 65 | 66 | func initContacts(ctx context.Context, n int, nbi *inventory.NetboxInventory) { 67 | for i := 0; i < n; i++ { 68 | contactName := fmt.Sprintf("Contact %d", i) 69 | _, err := nbi.AddContact(ctx, &objects.Contact{ 70 | Name: contactName, 71 | Email: fmt.Sprintf("user%d@example.com", i), 72 | }) 73 | if err != nil { 74 | fmt.Printf("Adding contact: %s", err) 75 | } 76 | } 77 | } 78 | 79 | func InitManufacturers(ctx context.Context, n int, nbi *inventory.NetboxInventory) { 80 | for i := 0; i < n; i++ { 81 | manufacturerName := fmt.Sprintf("Manufacturer %d", i) 82 | _, err := nbi.AddManufacturer(ctx, &objects.Manufacturer{ 83 | Name: manufacturerName, 84 | Slug: utils.Slugify(manufacturerName), 85 | }) 86 | if err != nil { 87 | fmt.Printf("Adding manufacturer: %s", err) 88 | } 89 | } 90 | } 91 | 92 | func InitPlatforms(ctx context.Context, n int, nbi *inventory.NetboxInventory) { 93 | for i := 0; i < n; i++ { 94 | manufacturer, _ := nbi.GetManufacturer( 95 | fmt.Sprintf("Manufacturer %d", i%NumberOfManufacturers), 96 | ) 97 | platformName := fmt.Sprintf("Platform %d", i) 98 | _, err := nbi.AddPlatform(ctx, &objects.Platform{ 99 | Name: platformName, 100 | Slug: utils.Slugify(platformName), 101 | Manufacturer: manufacturer, 102 | }) 103 | if err != nil { 104 | fmt.Printf("Adding platform: %s", err) 105 | } 106 | } 107 | } 108 | 109 | func InitVMs(ctx context.Context, n int, nbi *inventory.NetboxInventory) { 110 | for i := 0; i < n; i++ { 111 | vmName := fmt.Sprintf("VM %d", i) 112 | _, err := nbi.AddVM(ctx, &objects.VM{ 113 | Name: vmName, 114 | }) 115 | if err != nil { 116 | fmt.Printf("Adding VM: %s", err) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /cmd/netbox-ssot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "sync" 9 | "time" 10 | 11 | "github.com/src-doo/netbox-ssot/internal/constants" 12 | "github.com/src-doo/netbox-ssot/internal/logger" 13 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 14 | "github.com/src-doo/netbox-ssot/internal/parser" 15 | "github.com/src-doo/netbox-ssot/internal/source" 16 | "github.com/src-doo/netbox-ssot/internal/source/common" 17 | ) 18 | 19 | var configPath = flag.String("config", "config.yaml", "Path to the configuration file") 20 | 21 | // Build variables provided with ldflags. 22 | var ( 23 | version = "unknown" 24 | commit = "unknown" 25 | date = "unknown" 26 | ) 27 | 28 | func main() { 29 | // Print build information 30 | fmt.Printf("Running version %s built on %s (commit %s)\n\n", version, date, commit) 31 | 32 | startTime := time.Now() 33 | 34 | // Parse configuration 35 | fmt.Printf("Netbox-SSOT has started at %s\n", startTime.Format(time.RFC3339)) 36 | flag.Parse() 37 | config, err := parser.ParseConfig(*configPath) 38 | if err != nil { 39 | fmt.Println("Parser:", err) 40 | os.Exit(1) 41 | } 42 | 43 | // Create our main context 44 | mainCtx := context.Background() 45 | mainCtx = context.WithValue(mainCtx, constants.CtxSourceKey, "main") 46 | 47 | // Initialize Logger 48 | ssotLogger, err := logger.New(config.Logger.Dest, config.Logger.Level) 49 | if err != nil { 50 | fmt.Println("Logger:", err) 51 | os.Exit(1) 52 | } 53 | ssotLogger.Debug(mainCtx, "Parsed Logger config: ", config.Logger) 54 | ssotLogger.Debug(mainCtx, "Parsed Netbox config: ", config.Netbox) 55 | ssotLogger.Debug(mainCtx, "Parsed Source config: ", config.Sources) 56 | 57 | inventoryLogger, err := logger.New(config.Logger.Dest, config.Logger.Level) 58 | if err != nil { 59 | ssotLogger.Errorf(mainCtx, "inventoryLogger: %s", err) 60 | os.Exit(1) 61 | } 62 | inventoryCtx := context.WithValue(context.Background(), constants.CtxSourceKey, "inventory") 63 | netboxInventory := inventory.NewNetboxInventory(inventoryCtx, inventoryLogger, config.Netbox) 64 | ssotLogger.Debug(mainCtx, "Netbox inventory: ", netboxInventory) 65 | 66 | ssotLogger.Info(mainCtx, "Starting initializing netbox inventory") 67 | err = netboxInventory.Init() 68 | if err != nil { 69 | ssotLogger.Error(mainCtx, err) 70 | os.Exit(1) 71 | } 72 | ssotLogger.Debug(mainCtx, "Netbox inventory initialized: ", netboxInventory) 73 | 74 | // Variable to store if the run was successful. If it wasn't we don't remove orphans. 75 | successfullRun := true 76 | // Variable to store failed sourcesFalse 77 | encounteredErrors := map[string]error{} 78 | 79 | // Go through all sources and sync data 80 | var wg sync.WaitGroup 81 | for i := range config.Sources { 82 | sourceConfig := &config.Sources[i] 83 | ssotLogger.Info(mainCtx, "Processing source ", sourceConfig.Name, "...") 84 | sourceCtx := context.WithValue(mainCtx, constants.CtxSourceKey, sourceConfig.Name) 85 | source, err := source.NewSource(sourceCtx, sourceConfig, ssotLogger, netboxInventory) 86 | if err != nil { 87 | ssotLogger.Error(sourceCtx, err) 88 | os.Exit(1) 89 | } 90 | ssotLogger.Infof(sourceCtx, "Successfully created source %s", constants.CheckMark) 91 | ssotLogger.Debugf(sourceCtx, "Source content: %s", source) 92 | wg.Add(1) 93 | // Run each source in parallel 94 | go func(sourceCtx context.Context, source common.Source) { 95 | defer wg.Done() 96 | sourceName, ok := sourceCtx.Value(constants.CtxSourceKey).(string) 97 | if !ok { 98 | ssotLogger.Errorf(sourceCtx, "source ctx value is not set") 99 | return 100 | } 101 | // Source initialization 102 | ssotLogger.Info(sourceCtx, "Initializing source") 103 | err = source.Init() 104 | if err != nil { 105 | ssotLogger.Error(sourceCtx, err) 106 | successfullRun = false 107 | encounteredErrors[sourceName] = err 108 | return 109 | } 110 | ssotLogger.Infof(sourceCtx, "Successfully initialized source %s", constants.CheckMark) 111 | 112 | // Source synchronization 113 | ssotLogger.Info(sourceCtx, "Syncing source...") 114 | err = source.Sync(netboxInventory) 115 | if err != nil { 116 | successfullRun = false 117 | ssotLogger.Error(sourceCtx, err) 118 | encounteredErrors[sourceName] = err 119 | return 120 | } 121 | ssotLogger.Infof(sourceCtx, "Source synced successfully %s", constants.CheckMark) 122 | }(sourceCtx, source) 123 | } 124 | wg.Wait() 125 | 126 | // Orphan manager cleanup on successful run and if enabled 127 | if successfullRun { 128 | ssotLogger.Info(mainCtx, "Cleaning up orphaned objects...") 129 | err = netboxInventory.DeleteOrphans(config.Netbox.RemoveOrphans) 130 | if err != nil { 131 | ssotLogger.Error(mainCtx, err) 132 | os.Exit(1) 133 | } 134 | ssotLogger.Infof(mainCtx, "%s Successfully removed orphans", constants.CheckMark) 135 | } else { 136 | ssotLogger.Info(mainCtx, "Skipping removing orphaned objects because run failed...") 137 | } 138 | 139 | duration := time.Since(startTime) 140 | minutes := int(duration.Minutes()) 141 | seconds := int((duration - time.Duration(minutes)*time.Minute).Seconds()) 142 | if successfullRun { 143 | ssotLogger.Infof( 144 | mainCtx, 145 | "%s Syncing took %d min %d sec in total", 146 | constants.Rocket, 147 | minutes, 148 | seconds, 149 | ) 150 | } else { 151 | for source, err := range encounteredErrors { 152 | ssotLogger.Infof(mainCtx, "%s syncing of source %s failed with: %v", constants.WarningSign, source, err) 153 | } 154 | os.Exit(1) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "dictionaries": [ 3 | "project-words" 4 | ], 5 | "dictionaryDefinitions": [ 6 | { 7 | "name": "project-words", 8 | "path": "./project-words.txt" 9 | } 10 | ], 11 | "enabled": false, 12 | "ignorePaths": [ 13 | ".git/*", 14 | ".git/!{COMMIT_EDITMSG,EDITMSG}", 15 | ".git/*/**", 16 | ".gitignore", 17 | "action/lib/**", 18 | "cspell.json", 19 | "go.mod", 20 | "go.sum" 21 | ], 22 | "language": "en" 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/src-doo/netbox-ssot 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/PaloAltoNetworks/pango v0.10.2 7 | github.com/cisco-en-programmability/dnacenter-go-sdk/v7 v7.0.0 8 | github.com/luthermonson/go-proxmox v0.2.1 9 | github.com/ovirt/go-ovirt v4.3.4+incompatible 10 | github.com/scrapli/scrapligo v1.3.3 11 | github.com/src-doo/go-devicetype-library v0.1.56 12 | github.com/vmware/govmomi v0.48.1 13 | golang.org/x/text v0.23.0 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/buger/goterm v1.0.4 // indirect 19 | github.com/creack/pty v1.1.24 // indirect 20 | github.com/diskfs/go-diskfs v1.4.2 // indirect 21 | github.com/djherbis/times v1.6.0 // indirect 22 | github.com/go-resty/resty/v2 v2.16.5 // indirect 23 | github.com/google/go-querystring v1.1.0 // indirect 24 | github.com/gorilla/websocket v1.5.3 // indirect 25 | github.com/jinzhu/copier v0.4.0 // indirect 26 | github.com/magefile/mage v1.15.0 // indirect 27 | github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 // indirect 28 | golang.org/x/crypto v0.36.0 // indirect 29 | golang.org/x/net v0.38.0 // indirect 30 | golang.org/x/sys v0.31.0 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /internal/constants/symbols.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | CheckMark = "\u2713" 5 | Rocket = "\U0001F680" 6 | WarningSign = "\u26A0" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | 12 | "github.com/src-doo/netbox-ssot/internal/constants" 13 | ) 14 | 15 | // Default four levels for logging. 16 | const ( 17 | DEBUG int = iota 18 | INFO 19 | WARNING 20 | ERROR 21 | ) 22 | 23 | const logCallDepth = 2 24 | 25 | type Logger struct { 26 | *log.Logger 27 | // Level of the logger (DEBUG, INFO, WARNING, ERROR). 28 | level int 29 | } 30 | 31 | // New creates a new Logger instance, which writes to the specified destination (file) or stdout if dest is empty. 32 | // It also sets the log level. 33 | func New(dest string, logLevel int) (*Logger, error) { 34 | var output io.Writer 35 | if dest == "" { 36 | output = os.Stdout 37 | } else { 38 | file, err := os.Create(dest) 39 | if err != nil { 40 | return nil, err 41 | } 42 | output = file 43 | } 44 | return &Logger{log.New(output, "", log.LstdFlags), logLevel}, nil 45 | } 46 | 47 | // Custom log output function. It is used to add additional runtime information to the log message. 48 | func (l *Logger) Output(calldepth int, s string) error { 49 | // Get additional runtime information 50 | _, file, line, ok := runtime.Caller(calldepth) 51 | if !ok { 52 | file = "???" 53 | line = 0 54 | } else { 55 | file = filepath.Base(file) 56 | } 57 | 58 | // Prepare the log prefix manually to include the standard log flags 59 | 60 | // file prefix for logs 61 | filePrefix := fmt.Sprintf("%-20s", fmt.Sprintf("%s:%d", file, line)) 62 | if l.level > DEBUG { 63 | filePrefix = "" 64 | } 65 | 66 | // Add your custom logging format 67 | logMessage := fmt.Sprintf("%s%s", filePrefix, s) 68 | 69 | // Print to the desired output 70 | l.Println(logMessage) 71 | return nil 72 | } 73 | 74 | func (l *Logger) Debug(ctx context.Context, v ...interface{}) error { 75 | if l.level <= DEBUG { 76 | return l.Output( 77 | logCallDepth, 78 | fmt.Sprintf( 79 | "%-7s (%s): %s", 80 | "DEBUG", 81 | ctx.Value(constants.CtxSourceKey), 82 | fmt.Sprint(v...), 83 | ), 84 | ) 85 | } 86 | return nil 87 | } 88 | 89 | // Debugf logs a formatted debug message. 90 | func (l *Logger) Debugf(ctx context.Context, format string, v ...interface{}) error { 91 | if l.level <= DEBUG { 92 | return l.Output( 93 | logCallDepth, 94 | fmt.Sprintf( 95 | "%-7s (%s): %s", 96 | "DEBUG", 97 | ctx.Value(constants.CtxSourceKey), 98 | fmt.Sprintf(format, v...), 99 | ), 100 | ) 101 | } 102 | return nil 103 | } 104 | 105 | func (l *Logger) Info(ctx context.Context, v ...interface{}) error { 106 | if l.level <= INFO { 107 | return l.Output( 108 | logCallDepth, 109 | fmt.Sprintf( 110 | "%-7s (%s): %s", 111 | "INFO", 112 | ctx.Value(constants.CtxSourceKey), 113 | fmt.Sprint(v...), 114 | ), 115 | ) 116 | } 117 | return nil 118 | } 119 | 120 | // Infof logs a formatted info message. 121 | func (l *Logger) Infof(ctx context.Context, format string, v ...interface{}) error { 122 | if l.level <= INFO { 123 | return l.Output( 124 | logCallDepth, 125 | fmt.Sprintf( 126 | "%-7s (%s): %s", 127 | "INFO", 128 | ctx.Value(constants.CtxSourceKey), 129 | fmt.Sprintf(format, v...), 130 | ), 131 | ) 132 | } 133 | return nil 134 | } 135 | 136 | func (l *Logger) Warning(ctx context.Context, v ...interface{}) error { 137 | if l.level <= WARNING { 138 | return l.Output( 139 | logCallDepth, 140 | fmt.Sprintf( 141 | "%-7s (%s): %s", 142 | "WARNING", 143 | ctx.Value(constants.CtxSourceKey), 144 | fmt.Sprint(v...), 145 | ), 146 | ) 147 | } 148 | return nil 149 | } 150 | 151 | // Warningf logs a formatted warning message. 152 | func (l *Logger) Warningf(ctx context.Context, format string, v ...interface{}) error { 153 | if l.level <= WARNING { 154 | return l.Output( 155 | logCallDepth, 156 | fmt.Sprintf( 157 | "%-7s (%s): %s", 158 | "WARNING", 159 | ctx.Value(constants.CtxSourceKey), 160 | fmt.Sprintf(format, v...), 161 | ), 162 | ) 163 | } 164 | return nil 165 | } 166 | 167 | func (l *Logger) Error(ctx context.Context, v ...interface{}) error { 168 | if l.level <= ERROR { 169 | return l.Output( 170 | logCallDepth, 171 | fmt.Sprintf( 172 | "%-7s (%s): %s", 173 | "ERROR", 174 | ctx.Value(constants.CtxSourceKey), 175 | fmt.Sprint(v...), 176 | ), 177 | ) 178 | } 179 | return nil 180 | } 181 | 182 | // Errorf logs a formatted error message. 183 | func (l *Logger) Errorf(ctx context.Context, format string, v ...interface{}) error { 184 | if l.level <= ERROR { 185 | return l.Output( 186 | logCallDepth, 187 | fmt.Sprintf( 188 | "%-7s (%s): %s", 189 | "ERROR", 190 | ctx.Value(constants.CtxSourceKey), 191 | fmt.Sprintf(format, v...), 192 | ), 193 | ) 194 | } 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /internal/netbox/inventory/add_predefined_items.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/constants" 7 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 8 | "github.com/src-doo/netbox-ssot/internal/utils" 9 | ) 10 | 11 | func (nbi *NetboxInventory) AddContainerDeviceRole( 12 | ctx context.Context, 13 | ) (*objects.DeviceRole, error) { 14 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 15 | NetboxObject: objects.NetboxObject{ 16 | Description: constants.DeviceRoleContainerDescription, 17 | }, 18 | Name: constants.DeviceRoleContainer, 19 | Slug: utils.Slugify(constants.DeviceRoleContainer), 20 | Color: constants.DeviceRoleContainerColor, 21 | VMRole: true, 22 | }) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | return newRole, nil 28 | } 29 | 30 | func (nbi *NetboxInventory) AddFirewallDeviceRole( 31 | ctx context.Context, 32 | ) (*objects.DeviceRole, error) { 33 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 34 | NetboxObject: objects.NetboxObject{ 35 | Description: constants.DeviceRoleFirewallDescription, 36 | }, 37 | Name: constants.DeviceRoleFirewall, 38 | Slug: utils.Slugify(constants.DeviceRoleFirewall), 39 | Color: constants.DeviceRoleFirewallColor, 40 | VMRole: false, 41 | }) 42 | 43 | if err != nil { 44 | return nil, err 45 | } 46 | return newRole, nil 47 | } 48 | 49 | func (nbi *NetboxInventory) AddSwitchDeviceRole(ctx context.Context) (*objects.DeviceRole, error) { 50 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 51 | NetboxObject: objects.NetboxObject{ 52 | Description: constants.DeviceRoleSwitchDescription, 53 | }, 54 | Name: constants.DeviceRoleSwitch, 55 | Slug: utils.Slugify(constants.DeviceRoleSwitch), 56 | Color: constants.DeviceRoleSwitchColor, 57 | VMRole: false, 58 | }) 59 | 60 | if err != nil { 61 | return nil, err 62 | } 63 | return newRole, nil 64 | } 65 | 66 | func (nbi *NetboxInventory) AddServerDeviceRole(ctx context.Context) (*objects.DeviceRole, error) { 67 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 68 | NetboxObject: objects.NetboxObject{ 69 | Description: constants.DeviceRoleServerDescription, 70 | }, 71 | Name: constants.DeviceRoleServer, 72 | Slug: utils.Slugify(constants.DeviceRoleServer), 73 | Color: constants.DeviceRoleServerColor, 74 | VMRole: false, 75 | }) 76 | 77 | if err != nil { 78 | return nil, err 79 | } 80 | return newRole, nil 81 | } 82 | 83 | func (nbi *NetboxInventory) AddVMDeviceRole(ctx context.Context) (*objects.DeviceRole, error) { 84 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 85 | NetboxObject: objects.NetboxObject{ 86 | Description: constants.DeviceRoleVMDescription, 87 | }, 88 | Name: constants.DeviceRoleVM, 89 | Slug: utils.Slugify(constants.DeviceRoleVM), 90 | Color: constants.DeviceRoleVMColor, 91 | VMRole: false, 92 | }) 93 | 94 | if err != nil { 95 | return nil, err 96 | } 97 | return newRole, nil 98 | } 99 | 100 | func (nbi *NetboxInventory) AddVMTemplateDeviceRole( 101 | ctx context.Context, 102 | ) (*objects.DeviceRole, error) { 103 | newRole, err := nbi.AddDeviceRole(ctx, &objects.DeviceRole{ 104 | NetboxObject: objects.NetboxObject{ 105 | Description: constants.DeviceRoleVMTemplateDescription, 106 | }, 107 | Name: constants.DeviceRoleVMTemplate, 108 | Slug: utils.Slugify(constants.DeviceRoleVMTemplate), 109 | Color: constants.DeviceRoleVMTemplateColor, 110 | VMRole: false, 111 | }) 112 | 113 | if err != nil { 114 | return nil, err 115 | } 116 | return newRole, nil 117 | } 118 | -------------------------------------------------------------------------------- /internal/netbox/inventory/add_predefined_items_test.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 9 | ) 10 | 11 | func TestNetboxInventory_AddContainerDeviceRole(t *testing.T) { 12 | type args struct { 13 | ctx context.Context 14 | } 15 | tests := []struct { 16 | name string 17 | nbi *NetboxInventory 18 | args args 19 | want *objects.DeviceRole 20 | wantErr bool 21 | }{ 22 | // TODO: Add test cases. 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | got, err := tt.nbi.AddContainerDeviceRole(tt.args.ctx) 27 | if (err != nil) != tt.wantErr { 28 | t.Errorf( 29 | "NetboxInventory.AddContainerDeviceRole() error = %v, wantErr %v", 30 | err, 31 | tt.wantErr, 32 | ) 33 | return 34 | } 35 | if !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("NetboxInventory.AddContainerDeviceRole() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestNetboxInventory_AddFirewallDeviceRole(t *testing.T) { 43 | type args struct { 44 | ctx context.Context 45 | } 46 | tests := []struct { 47 | name string 48 | nbi *NetboxInventory 49 | args args 50 | want *objects.DeviceRole 51 | wantErr bool 52 | }{ 53 | // TODO: Add test cases. 54 | } 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | got, err := tt.nbi.AddFirewallDeviceRole(tt.args.ctx) 58 | if (err != nil) != tt.wantErr { 59 | t.Errorf( 60 | "NetboxInventory.AddFirewallDeviceRole() error = %v, wantErr %v", 61 | err, 62 | tt.wantErr, 63 | ) 64 | return 65 | } 66 | if !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("NetboxInventory.AddFirewallDeviceRole() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestNetboxInventory_AddSwitchDeviceRole(t *testing.T) { 74 | type args struct { 75 | ctx context.Context 76 | } 77 | tests := []struct { 78 | name string 79 | nbi *NetboxInventory 80 | args args 81 | want *objects.DeviceRole 82 | wantErr bool 83 | }{ 84 | // TODO: Add test cases. 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | got, err := tt.nbi.AddSwitchDeviceRole(tt.args.ctx) 89 | if (err != nil) != tt.wantErr { 90 | t.Errorf( 91 | "NetboxInventory.AddSwitchDeviceRole() error = %v, wantErr %v", 92 | err, 93 | tt.wantErr, 94 | ) 95 | return 96 | } 97 | if !reflect.DeepEqual(got, tt.want) { 98 | t.Errorf("NetboxInventory.AddSwitchDeviceRole() = %v, want %v", got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestNetboxInventory_AddServerDeviceRole(t *testing.T) { 105 | type args struct { 106 | ctx context.Context 107 | } 108 | tests := []struct { 109 | name string 110 | nbi *NetboxInventory 111 | args args 112 | want *objects.DeviceRole 113 | wantErr bool 114 | }{ 115 | // TODO: Add test cases. 116 | } 117 | for _, tt := range tests { 118 | t.Run(tt.name, func(t *testing.T) { 119 | got, err := tt.nbi.AddServerDeviceRole(tt.args.ctx) 120 | if (err != nil) != tt.wantErr { 121 | t.Errorf( 122 | "NetboxInventory.AddServerDeviceRole() error = %v, wantErr %v", 123 | err, 124 | tt.wantErr, 125 | ) 126 | return 127 | } 128 | if !reflect.DeepEqual(got, tt.want) { 129 | t.Errorf("NetboxInventory.AddServerDeviceRole() = %v, want %v", got, tt.want) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestNetboxInventory_AddVMDeviceRole(t *testing.T) { 136 | type args struct { 137 | ctx context.Context 138 | } 139 | tests := []struct { 140 | name string 141 | nbi *NetboxInventory 142 | args args 143 | want *objects.DeviceRole 144 | wantErr bool 145 | }{ 146 | // TODO: Add test cases. 147 | } 148 | for _, tt := range tests { 149 | t.Run(tt.name, func(t *testing.T) { 150 | got, err := tt.nbi.AddVMDeviceRole(tt.args.ctx) 151 | if (err != nil) != tt.wantErr { 152 | t.Errorf( 153 | "NetboxInventory.AddVMDeviceRole() error = %v, wantErr %v", 154 | err, 155 | tt.wantErr, 156 | ) 157 | return 158 | } 159 | if !reflect.DeepEqual(got, tt.want) { 160 | t.Errorf("NetboxInventory.AddVMDeviceRole() = %v, want %v", got, tt.want) 161 | } 162 | }) 163 | } 164 | } 165 | 166 | func TestNetboxInventory_AddVMTemplateDeviceRole(t *testing.T) { 167 | type args struct { 168 | ctx context.Context 169 | } 170 | tests := []struct { 171 | name string 172 | nbi *NetboxInventory 173 | args args 174 | want *objects.DeviceRole 175 | wantErr bool 176 | }{ 177 | // TODO: Add test cases. 178 | } 179 | for _, tt := range tests { 180 | t.Run(tt.name, func(t *testing.T) { 181 | got, err := tt.nbi.AddVMTemplateDeviceRole(tt.args.ctx) 182 | if (err != nil) != tt.wantErr { 183 | t.Errorf( 184 | "NetboxInventory.AddVMTemplateDeviceRole() error = %v, wantErr %v", 185 | err, 186 | tt.wantErr, 187 | ) 188 | return 189 | } 190 | if !reflect.DeepEqual(got, tt.want) { 191 | t.Errorf("NetboxInventory.AddVMTemplateDeviceRole() = %v, want %v", got, tt.want) 192 | } 193 | }) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /internal/netbox/inventory/delete_items_test.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 7 | ) 8 | 9 | func TestNetboxInventory_DeleteOrphans(t *testing.T) { 10 | type args struct { 11 | hard bool 12 | } 13 | tests := []struct { 14 | name string 15 | nbi *NetboxInventory 16 | args args 17 | wantErr bool 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if err := tt.nbi.DeleteOrphans(tt.args.hard); (err != nil) != tt.wantErr { 24 | t.Errorf("NetboxInventory.DeleteOrphans() error = %v, wantErr %v", err, tt.wantErr) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func TestNetboxInventory_hardDelete(t *testing.T) { 31 | type args struct { 32 | orphanItem objects.OrphanItem 33 | } 34 | tests := []struct { 35 | name string 36 | nbi *NetboxInventory 37 | args args 38 | wantErr bool 39 | }{ 40 | // TODO: Add test cases. 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if err := tt.nbi.hardDelete(tt.args.orphanItem); (err != nil) != tt.wantErr { 45 | t.Errorf("NetboxInventory.hardDelete() error = %v, wantErr %v", err, tt.wantErr) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/netbox/inventory/inventory_test.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/src-doo/netbox-ssot/internal/logger" 9 | "github.com/src-doo/netbox-ssot/internal/parser" 10 | ) 11 | 12 | func TestNetboxInventory_String(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | nbi *NetboxInventory 16 | want string 17 | }{ 18 | // TODO: Add test cases. 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := tt.nbi.String(); got != tt.want { 23 | t.Errorf("NetboxInventory.String() = %v, want %v", got, tt.want) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestNewNetboxInventory(t *testing.T) { 30 | type args struct { 31 | ctx context.Context 32 | logger *logger.Logger 33 | nbConfig *parser.NetboxConfig 34 | } 35 | tests := []struct { 36 | name string 37 | args args 38 | want *NetboxInventory 39 | }{ 40 | // TODO: Add test cases. 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := NewNetboxInventory(tt.args.ctx, tt.args.logger, tt.args.nbConfig); !reflect.DeepEqual( 45 | got, 46 | tt.want, 47 | ) { 48 | t.Errorf("NewNetboxInventory() = %v, want %v", got, tt.want) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestNetboxInventory_Init(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | nbi *NetboxInventory 58 | wantErr bool 59 | }{ 60 | // TODO: Add test cases. 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if err := tt.nbi.Init(); (err != nil) != tt.wantErr { 65 | t.Errorf("NetboxInventory.Init() error = %v, wantErr %v", err, tt.wantErr) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestNetboxInventory_checkVersion(t *testing.T) { 72 | tests := []struct { 73 | name string 74 | nbi *NetboxInventory 75 | wantErr bool 76 | }{ 77 | // TODO: Add test cases. 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | if err := tt.nbi.checkVersion(); (err != nil) != tt.wantErr { 82 | t.Errorf("NetboxInventory.checkVersion() error = %v, wantErr %v", err, tt.wantErr) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/netbox/inventory/orphan_manager.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/constants" 7 | "github.com/src-doo/netbox-ssot/internal/logger" 8 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 9 | ) 10 | 11 | type OrphanManager struct { 12 | // ItemsSet is a map of objectAPIPath to a set of managed ids for that object type. 13 | // 14 | // { 15 | // "/api/dcim/devices/": {22: true, 3: true, ...}, 16 | // "/api/dcim/interface/": {15: true, 36: true, ...}, 17 | // "/api/virtualization/clusters/": {121: true, 122: true, ...}, 18 | // "...": [...] 19 | // } 20 | // 21 | // It stores which objects have been created by netbox-ssot and can be deleted 22 | // because they are not available in the sources anymore 23 | Items map[constants.APIPath]map[int]objects.OrphanItem 24 | // OrphanObjectPriority is a map that stores priorities for each object. This is necessary 25 | // because map order is non deterministic and if we delete dependent object first we will 26 | // get the dependency error. 27 | // 28 | // { 29 | // 0: service.TagApiPath, 30 | // 1: service.CustomFieldApiPath, 31 | // ... 32 | // } 33 | OrphanObjectPriority map[int]constants.APIPath 34 | // Tag for orphaned objects. Initialized in initTags. 35 | Tag *objects.Tag 36 | // Logger for orphan manager 37 | Logger *logger.Logger 38 | // Context for orphan manager 39 | Ctx context.Context 40 | } 41 | 42 | func NewOrphanManager(logger *logger.Logger) *OrphanManager { 43 | // Starts with 0 for easier integration with for loops 44 | orphanObjectPriority := map[int]constants.APIPath{ 45 | 0: constants.VlanGroupsAPIPath, 46 | 1: constants.PrefixesAPIPath, 47 | 2: constants.VlansAPIPath, 48 | 3: constants.IPAddressesAPIPath, 49 | 4: constants.VirtualDeviceContextsAPIPath, 50 | 5: constants.InterfacesAPIPath, 51 | 6: constants.VMInterfacesAPIPath, 52 | 7: constants.VirtualDisksAPIPath, 53 | 8: constants.VirtualMachinesAPIPath, 54 | 9: constants.DevicesAPIPath, 55 | 10: constants.PlatformsAPIPath, 56 | 11: constants.DeviceTypesAPIPath, 57 | 12: constants.ManufacturersAPIPath, 58 | 13: constants.DeviceRolesAPIPath, 59 | 14: constants.ClustersAPIPath, 60 | 15: constants.ClusterTypesAPIPath, 61 | 16: constants.ClusterGroupsAPIPath, 62 | 17: constants.ContactAssignmentsAPIPath, 63 | 18: constants.ContactsAPIPath, 64 | 19: constants.WirelessLANsAPIPath, 65 | 20: constants.WirelessLANGroupsAPIPath, 66 | 21: constants.MACAddressesAPIPath, 67 | } 68 | orphanCtx := context.WithValue(context.Background(), constants.CtxSourceKey, "orphanManager") 69 | 70 | return &OrphanManager{ 71 | Items: map[constants.APIPath]map[int]objects.OrphanItem{}, 72 | OrphanObjectPriority: orphanObjectPriority, 73 | Logger: logger, 74 | Ctx: orphanCtx, 75 | } 76 | } 77 | 78 | func (orphanManager *OrphanManager) AddItem(orphanItem objects.OrphanItem) { 79 | // Manage only objects created with netbox-ssot tag 80 | netboxObject := orphanItem.GetNetboxObject() 81 | if netboxObject.HasTagByName(constants.SsotTagName) { 82 | if orphanManager.Items[orphanItem.GetAPIPath()] == nil { 83 | orphanManager.Items[orphanItem.GetAPIPath()] = map[int]objects.OrphanItem{} 84 | } 85 | orphanManager.Items[orphanItem.GetAPIPath()][netboxObject.ID] = orphanItem 86 | } 87 | } 88 | 89 | func (orphanManager *OrphanManager) RemoveItem(obj objects.OrphanItem) { 90 | delete(orphanManager.Items[obj.GetAPIPath()], obj.GetID()) 91 | } 92 | -------------------------------------------------------------------------------- /internal/netbox/inventory/orphan_manager_test.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/src-doo/netbox-ssot/internal/logger" 8 | ) 9 | 10 | func TestNewOrphanManager(t *testing.T) { 11 | type args struct { 12 | logger *logger.Logger 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want *OrphanManager 18 | }{ 19 | // TODO: Add test cases. 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | if got := NewOrphanManager(tt.args.logger); !reflect.DeepEqual(got, tt.want) { 24 | t.Errorf("NewOrphanManager() = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/netbox/inventory/test_objects.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "sync" 8 | 9 | "github.com/src-doo/netbox-ssot/internal/constants" 10 | "github.com/src-doo/netbox-ssot/internal/logger" 11 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 12 | "github.com/src-doo/netbox-ssot/internal/netbox/service" 13 | ) 14 | 15 | var MockExistingTags = map[string]*objects.Tag{ 16 | "existing_tag1": { 17 | Name: "existing_tag1", 18 | Description: "Test exististing tag1", 19 | Slug: "existing_tag1", 20 | }, 21 | "existing_tag2": { 22 | Name: "existing_tag2", 23 | Description: "Test exististing tag2", 24 | Slug: "existing_tag2", 25 | }, 26 | } 27 | 28 | var MockExistingTenants = map[string]*objects.Tenant{ 29 | "existing_tenant1": { 30 | NetboxObject: objects.NetboxObject{ 31 | ID: 1, 32 | Tags: []*objects.Tag{service.MockDefaultSsotTag}, 33 | }, 34 | Name: "existing_tenant1", 35 | Slug: "existing_tenant1", 36 | }, 37 | "existing_tenant2": { 38 | NetboxObject: objects.NetboxObject{ 39 | ID: 2, //nolint:mnd 40 | Tags: []*objects.Tag{service.MockDefaultSsotTag}, 41 | }, 42 | Name: "existing_tenant2", 43 | Slug: "existing_tenant2", 44 | }, 45 | } 46 | 47 | var MockExistingSites = map[string]*objects.Site{ 48 | "existing_site1": { 49 | NetboxObject: objects.NetboxObject{ 50 | ID: 1, 51 | Tags: []*objects.Tag{service.MockDefaultSsotTag}, 52 | }, 53 | Name: "existing_site1", 54 | Slug: "existing_site1", 55 | }, 56 | "existing_site2": { 57 | NetboxObject: objects.NetboxObject{ 58 | ID: 2, //nolint:mnd 59 | Tags: []*objects.Tag{service.MockDefaultSsotTag}, 60 | }, 61 | Name: "existing_site2", 62 | Slug: "existing_site2", 63 | }, 64 | } 65 | 66 | var MockInventory = &NetboxInventory{ 67 | Logger: &logger.Logger{Logger: log.New(os.Stdout, "", log.LstdFlags)}, 68 | tagsIndexByName: MockExistingTags, 69 | tagsLock: sync.Mutex{}, 70 | tenantsIndexByName: MockExistingTenants, 71 | tenantsLock: sync.Mutex{}, 72 | sitesIndexByName: MockExistingSites, 73 | sitesLock: sync.Mutex{}, 74 | NetboxAPI: service.MockNetboxClient, 75 | Ctx: context.WithValue( 76 | context.Background(), 77 | constants.CtxSourceKey, 78 | "testInventory", 79 | ), 80 | SsotTag: &objects.Tag{ 81 | ID: 0, 82 | Name: "netbox-ssot", 83 | Slug: "netbox-ssot", 84 | Description: "default netbox-ssot tag", 85 | Color: "ffffff", 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /internal/netbox/mapper/mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/constants" 7 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 8 | ) 9 | 10 | func reverseMap(m map[reflect.Type]constants.APIPath) map[constants.APIPath]reflect.Type { 11 | reversed := make(map[constants.APIPath]reflect.Type, len(m)) 12 | for k, v := range m { 13 | reversed[v] = k 14 | } 15 | return reversed 16 | } 17 | 18 | var Type2Path = map[reflect.Type]constants.APIPath{ 19 | reflect.TypeOf((*objects.VlanGroup)(nil)).Elem(): constants.VlanGroupsAPIPath, 20 | reflect.TypeOf((*objects.Vlan)(nil)).Elem(): constants.VlansAPIPath, 21 | reflect.TypeOf((*objects.IPAddress)(nil)).Elem(): constants.IPAddressesAPIPath, 22 | reflect.TypeOf((*objects.ClusterType)(nil)).Elem(): constants.ClusterTypesAPIPath, 23 | reflect.TypeOf((*objects.ClusterGroup)(nil)).Elem(): constants.ClusterGroupsAPIPath, 24 | reflect.TypeOf((*objects.Cluster)(nil)).Elem(): constants.ClustersAPIPath, 25 | reflect.TypeOf((*objects.VM)(nil)).Elem(): constants.VirtualMachinesAPIPath, 26 | reflect.TypeOf((*objects.VMInterface)(nil)).Elem(): constants.VMInterfacesAPIPath, 27 | reflect.TypeOf((*objects.Device)(nil)).Elem(): constants.DevicesAPIPath, 28 | reflect.TypeOf((*objects.MACAddress)(nil)).Elem(): constants.MACAddressesAPIPath, 29 | reflect.TypeOf((*objects.VirtualDeviceContext)(nil)).Elem(): constants.VirtualDeviceContextsAPIPath, 30 | reflect.TypeOf((*objects.DeviceRole)(nil)).Elem(): constants.DeviceRolesAPIPath, 31 | reflect.TypeOf((*objects.DeviceType)(nil)).Elem(): constants.DeviceTypesAPIPath, 32 | reflect.TypeOf((*objects.Interface)(nil)).Elem(): constants.InterfacesAPIPath, 33 | reflect.TypeOf((*objects.Site)(nil)).Elem(): constants.SitesAPIPath, 34 | reflect.TypeOf((*objects.SiteGroup)(nil)).Elem(): constants.SiteGroupsAPIPath, 35 | reflect.TypeOf((*objects.Manufacturer)(nil)).Elem(): constants.ManufacturersAPIPath, 36 | reflect.TypeOf((*objects.Platform)(nil)).Elem(): constants.PlatformsAPIPath, 37 | reflect.TypeOf((*objects.Tenant)(nil)).Elem(): constants.TenantsAPIPath, 38 | reflect.TypeOf((*objects.ContactGroup)(nil)).Elem(): constants.ContactGroupsAPIPath, 39 | reflect.TypeOf((*objects.ContactRole)(nil)).Elem(): constants.ContactRolesAPIPath, 40 | reflect.TypeOf((*objects.Contact)(nil)).Elem(): constants.ContactsAPIPath, 41 | reflect.TypeOf((*objects.CustomField)(nil)).Elem(): constants.CustomFieldsAPIPath, 42 | reflect.TypeOf((*objects.Tag)(nil)).Elem(): constants.TagsAPIPath, 43 | reflect.TypeOf((*objects.ContactAssignment)(nil)).Elem(): constants.ContactAssignmentsAPIPath, 44 | reflect.TypeOf((*objects.Prefix)(nil)).Elem(): constants.PrefixesAPIPath, 45 | reflect.TypeOf((*objects.WirelessLAN)(nil)).Elem(): constants.WirelessLANsAPIPath, 46 | reflect.TypeOf((*objects.WirelessLANGroup)(nil)).Elem(): constants.WirelessLANGroupsAPIPath, 47 | reflect.TypeOf((*objects.VirtualDisk)(nil)).Elem(): constants.VirtualDisksAPIPath, 48 | } 49 | 50 | var Path2Type = reverseMap(Type2Path) 51 | -------------------------------------------------------------------------------- /internal/netbox/objects/common.go: -------------------------------------------------------------------------------- 1 | // This file contains all objects that are common to all Netbox objects. 2 | package objects 3 | 4 | import ( 5 | "fmt" 6 | "slices" 7 | ) 8 | 9 | // Choice represents a choice in a Netbox's choice field. 10 | // This struct is used as an embedded struct in other structs that represent Choice fields. 11 | type Choice struct { 12 | Value string `json:"value,omitempty"` 13 | Label string `json:"label,omitempty"` 14 | } 15 | 16 | func (c Choice) String() string { 17 | return c.Value 18 | } 19 | 20 | const ( 21 | MaxDescriptionLength = 200 22 | ) 23 | 24 | // Struct representing attributes that are common to all objects in Netbox. 25 | // We can this struct as an embedded struct in other structs that represent 26 | // Netbox objects. 27 | type NetboxObject struct { 28 | // Netbox's ID of the object. 29 | ID int `json:"id,omitempty"` 30 | // List of tags assigned to this object. 31 | Tags []*Tag `json:"tags,omitempty"` 32 | // Description represents custom description of the object. 33 | Description string `json:"description,omitempty"` 34 | // Array of custom fields, in format customFieldLabel: customFieldValue 35 | CustomFields map[string]interface{} `json:"custom_fields,omitempty"` 36 | } 37 | 38 | func (n NetboxObject) String() string { 39 | return fmt.Sprintf("ID: %d, Tags: %s, Description: %s", n.ID, n.Tags, n.Description) 40 | } 41 | 42 | func (n *NetboxObject) GetID() int { 43 | return n.ID 44 | } 45 | 46 | func (n *NetboxObject) GetCustomField(label string) interface{} { 47 | if n.CustomFields == nil { 48 | return nil 49 | } 50 | return n.CustomFields[label] 51 | } 52 | 53 | func (n *NetboxObject) SetCustomField(label string, value interface{}) { 54 | if n.CustomFields == nil { 55 | n.CustomFields = make(map[string]interface{}) 56 | } 57 | n.CustomFields[label] = value 58 | } 59 | 60 | // AddTag adds a tag to the NetboxObject if 61 | // it doesn't have it already. If the tag is already present, 62 | // nothing happens. 63 | func (n *NetboxObject) AddTag(newTag *Tag) { 64 | if slices.IndexFunc(n.Tags, func(t *Tag) bool { 65 | return t.Name == newTag.Name 66 | }) == -1 { 67 | n.Tags = append(n.Tags, newTag) 68 | } 69 | } 70 | 71 | // HasTag checks if the NetboxObject has a tag. 72 | // It returns true if the object has the tag, otherwise false. 73 | func (n *NetboxObject) HasTag(tag *Tag) bool { 74 | return slices.IndexFunc(n.Tags, func(t *Tag) bool { 75 | return t.Name == tag.Name 76 | }) >= 0 77 | } 78 | 79 | // HasTagByName checks if the NetboxObject has a tag by name. 80 | // It returns true if the object has the tag, otherwise false. 81 | func (n *NetboxObject) HasTagByName(tagName string) bool { 82 | return slices.IndexFunc(n.Tags, func(t *Tag) bool { 83 | return t.Name == tagName 84 | }) >= 0 85 | } 86 | 87 | // RemoveTag removes a tag from the NetboxObject. 88 | // If the tag is not present, nothing happens. 89 | func (n *NetboxObject) RemoveTag(tag *Tag) { 90 | index := slices.IndexFunc(n.Tags, func(t *Tag) bool { 91 | return t.Name == tag.Name 92 | }) 93 | if index >= 0 { 94 | n.Tags = append(n.Tags[:index], n.Tags[index+1:]...) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/netbox/objects/extras.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/constants" 7 | ) 8 | 9 | type Tag struct { 10 | ID int `json:"id,omitempty"` 11 | Name string `json:"name,omitempty"` 12 | Slug string `json:"slug,omitempty"` 13 | Color constants.Color `json:"color,omitempty"` 14 | Description string `json:"description,omitempty"` 15 | } 16 | 17 | func (t Tag) String() string { 18 | return fmt.Sprintf("Tag{Name: %s}", t.Name) 19 | } 20 | 21 | // Tag implements IDItem interface. 22 | func (t *Tag) GetID() int { 23 | return t.ID 24 | } 25 | func (t *Tag) GetObjectType() constants.ContentType { 26 | return constants.ContentTypeExtrasTag 27 | } 28 | 29 | func (t *Tag) GetAPIPath() constants.APIPath { 30 | return constants.TagsAPIPath 31 | } 32 | 33 | // CustomFieldTypes are predefined netbox's types for CustomFields. 34 | type CustomFieldType struct { 35 | Choice 36 | } 37 | 38 | // Predefined netbox's types for CustomFields 39 | // https://github.com/netbox-community/netbox/blob/35be4f05ef376e28d9af4d7245ba10cc286bb62a/netbox/extras/choices.py#L10 40 | var ( 41 | CustomFieldTypeText = CustomFieldType{Choice{Value: "text", Label: "Text"}} 42 | CustomFieldTypeLongText = CustomFieldType{Choice{Value: "longtext", Label: "Text (long)"}} 43 | CustomFieldTypeInteger = CustomFieldType{Choice{Value: "integer", Label: "Integer"}} 44 | CustomFieldTypeDecimal = CustomFieldType{Choice{Value: "decimal", Label: "Decimal"}} 45 | CustomFieldTypeBoolean = CustomFieldType{ 46 | Choice{Value: "boolean", Label: "Boolean (true/false)"}, 47 | } 48 | CustomFieldTypeDate = CustomFieldType{Choice{Value: "date", Label: "Date"}} 49 | ) 50 | 51 | type FilterLogic struct { 52 | Choice 53 | } 54 | 55 | var ( 56 | FilterLogicLoose = FilterLogic{Choice{Value: "loose", Label: "Loose"}} 57 | ) 58 | 59 | type CustomFieldUIVisible struct { 60 | Choice 61 | } 62 | 63 | var ( 64 | CustomFieldUIVisibleAlways = CustomFieldUIVisible{Choice{Value: "always", Label: "Always"}} 65 | CustomFieldUIVisibleIfSet = CustomFieldUIVisible{Choice{Value: "if-set", Label: "If set"}} 66 | CustomFieldUIVisibleHidden = CustomFieldUIVisible{Choice{Value: "hidden", Label: "Hidden"}} 67 | ) 68 | 69 | type CustomFieldUIEditable struct { 70 | Choice 71 | } 72 | 73 | var ( 74 | CustomFieldUIEditableYes = CustomFieldUIEditable{Choice{Value: "yes", Label: "Yes"}} 75 | CustomFieldUIEditableNo = CustomFieldUIEditable{Choice{Value: "no", Label: "No"}} 76 | CustomFieldUIEditableHidden = CustomFieldUIEditable{Choice{Value: "hidden", Label: "Hidden"}} 77 | ) 78 | 79 | const ( 80 | DisplayWeightDefault = 100 81 | SearchWeightDefault = 1000 82 | ) 83 | 84 | type CustomField struct { 85 | ID int `json:"id,omitempty"` 86 | // Name of the custom field (e.g. host_cpu_cores). This field is required. 87 | Name string `json:"name,omitempty"` 88 | // Label represents name of the field as displayed to users (e.g. Physical CPU cores). 89 | // If not provided, the name will be used instead. 90 | Label string `json:"label,omitempty"` 91 | // Type is the type of the custom field. 92 | // Valid choices are: text, integer, boolean, date, url, select, multiselect. This field is required. 93 | Type CustomFieldType `json:"type,omitempty"` 94 | // Type of the related object (for object/multi-object fields only) (e.g. objects.device). This field is required. 95 | ObjectTypes []constants.ContentType `json:"object_types,omitempty"` 96 | // Description is a description of the field. This field is optional. 97 | Description string `json:"description,omitempty"` 98 | // Weighting for search. Lower values are considered more important. Default (1000). 99 | SearchWeight int `json:"search_weight,omitempty"` 100 | // Filter logic. This field is required. (Default loose). 101 | FilterLogic FilterLogic `json:"filter_logic,omitempty"` 102 | // UI visible. This field is required. (Default read-write). 103 | CustomFieldUIVisible *CustomFieldUIVisible `json:"ui_visible,omitempty"` 104 | // UI editable. This field is required. (Default read-write). 105 | CustomFieldUIEditable *CustomFieldUIEditable `json:"ui_editable,omitempty"` 106 | // Display Weight. Fields with higher weights appear lower in a form (default is 100). 107 | DisplayWeight int `json:"weight,omitempty"` 108 | // Default value for the field (must be a JSON value). Encapsulate strings with double quotes (e.g. "Foo"). 109 | Default interface{} `json:"default"` 110 | // If this field is required or not. 111 | Required bool `json:"required"` 112 | } 113 | 114 | func (cf CustomField) String() string { 115 | return fmt.Sprintf("CustomField{ID: %d, Name: %s}", cf.ID, cf.Name) 116 | } 117 | 118 | // CustomField implements IDItem interface. 119 | func (cf *CustomField) GetID() int { 120 | return cf.ID 121 | } 122 | func (cf *CustomField) GetObjectType() constants.ContentType { 123 | return constants.ContentTypeExtrasCustomField 124 | } 125 | func (cf *CustomField) GetAPIPath() constants.APIPath { 126 | return constants.CustomFieldsAPIPath 127 | } 128 | -------------------------------------------------------------------------------- /internal/netbox/objects/extras_test.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTag_String(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | tr Tag 11 | want string 12 | }{ 13 | { 14 | name: "Test tag correct string", 15 | tr: Tag{ 16 | Name: "Test tag", 17 | Description: "Test tag description", 18 | }, 19 | want: "Tag{Name: Test tag}", 20 | }, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | if got := tt.tr.String(); got != tt.want { 25 | t.Errorf("Tag.String() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestCustomField_String(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | cf CustomField 35 | want string 36 | }{ 37 | { 38 | name: "Test custom field correct string", 39 | cf: CustomField{ 40 | ID: 10, 41 | Name: "host_cpu_cores", 42 | }, 43 | want: "CustomField{ID: 10, Name: host_cpu_cores}", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | if got := tt.cf.String(); got != tt.want { 49 | t.Errorf("CustomField.String() = %v, want %v", got, tt.want) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | func TestTag_GetID(t *testing.T) { 56 | tests := []struct { 57 | name string 58 | tr *Tag 59 | want int 60 | }{ 61 | { 62 | name: "Test tag get id", 63 | tr: &Tag{ 64 | ID: 1, 65 | }, 66 | want: 1, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | if got := tt.tr.GetID(); got != tt.want { 72 | t.Errorf("Tag.GetID() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestCustomField_GetID(t *testing.T) { 79 | tests := []struct { 80 | name string 81 | cf *CustomField 82 | want int 83 | }{ 84 | { 85 | name: "Test Custom Field Get ID", 86 | cf: &CustomField{ 87 | ID: 1, 88 | }, 89 | want: 1, 90 | }, 91 | } 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | if got := tt.cf.GetID(); got != tt.want { 95 | t.Errorf("CustomField.GetID() = %v, want %v", got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/netbox/objects/generic_objects.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import "github.com/src-doo/netbox-ssot/internal/constants" 4 | 5 | type IDItem interface { 6 | GetID() int 7 | GetObjectType() constants.ContentType 8 | GetAPIPath() constants.APIPath 9 | } 10 | 11 | type OrphanItem interface { 12 | GetID() int 13 | GetObjectType() constants.ContentType 14 | GetAPIPath() constants.APIPath 15 | GetNetboxObject() *NetboxObject 16 | } 17 | 18 | type MACAddressOwner interface { 19 | GetID() int 20 | GetObjectType() constants.ContentType 21 | GetAPIPath() constants.APIPath 22 | GetPrimaryMACAddress() *MACAddress 23 | SetPrimaryMACAddress(mac *MACAddress) 24 | } 25 | 26 | type IPAddressOwner interface { 27 | GetID() int 28 | GetObjectType() constants.ContentType 29 | GetAPIPath() constants.APIPath 30 | GetPrimaryIPv4Address() *IPAddress 31 | GetPrimaryIPv6Address() *IPAddress 32 | SetPrimaryIPAddress(ip *IPAddress) 33 | SetPrimaryIPv6Address(ip *IPAddress) 34 | } 35 | -------------------------------------------------------------------------------- /internal/netbox/objects/wireless.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/src-doo/netbox-ssot/internal/constants" 7 | ) 8 | 9 | type WirelessLANGroup struct { 10 | NetboxObject 11 | // Name is the name of the wireless lan group. This field is required. 12 | Name string `json:"name,omitempty"` 13 | // Slug is the slug of the wireless lan group. This field is required. 14 | Slug string `json:"slug,omitempty"` 15 | // Parent is the parent wireless lan group. 16 | Parent *WirelessLANGroup `json:"parent,omitempty"` 17 | } 18 | 19 | func (wlg WirelessLANGroup) String() string { 20 | return fmt.Sprintf("WirelessLANGroup{Name: %s, Slug: %s}", wlg.Name, wlg.Slug) 21 | } 22 | 23 | // WirelessLANGroup implements IDItem interface. 24 | func (wlg *WirelessLANGroup) GetID() int { 25 | return wlg.ID 26 | } 27 | func (wlg *WirelessLANGroup) GetObjectType() constants.ContentType { 28 | return constants.ContentTypeWirelessLANGroup 29 | } 30 | func (wlg *WirelessLANGroup) GetAPIPath() constants.APIPath { 31 | return constants.WirelessLANGroupsAPIPath 32 | } 33 | 34 | // WirelessLANGroup implements OrphanItem interface. 35 | func (wlg *WirelessLANGroup) GetNetboxObject() *NetboxObject { 36 | return &wlg.NetboxObject 37 | } 38 | 39 | type WirelessLANStatus struct { 40 | Choice 41 | } 42 | 43 | var ( 44 | WirelessLanStatusActive = WirelessLANStatus{Choice{Value: "active", Label: "Active"}} 45 | WirelessLanStatusReserved = WirelessLANStatus{Choice{Value: "reserved", Label: "Reserved"}} 46 | WirelessLanStatusDisabled = WirelessLANStatus{Choice{Value: "disabled", Label: "Disabled"}} 47 | WirelessLanStatusDeprecated = WirelessLANStatus{ 48 | Choice{Value: "deprecated", Label: "Deprecated"}, 49 | } 50 | ) 51 | 52 | type WirelessLANAuthType struct { 53 | Choice 54 | } 55 | 56 | var ( 57 | WirelessLanAuthTypeOpen = WirelessLANAuthType{Choice{Value: "open", Label: "Open"}} 58 | WirelessLanAuthTypeWep = WirelessLANAuthType{Choice{Value: "wep", Label: "WEP"}} 59 | WirelessLanAuthTypeWpaPersonal = WirelessLANAuthType{ 60 | Choice{Value: "wpa-personal", Label: "WPA Personal (PSK)"}, 61 | } 62 | WirelessLanAuthTypeWpaEnterprise = WirelessLANAuthType{ 63 | Choice{Value: "wpa-enterprise", Label: "WPA Enterprise"}, 64 | } 65 | ) 66 | 67 | type WirelessLANAuthCipher struct { 68 | Choice 69 | } 70 | 71 | var ( 72 | WirelessLANAuthCipherAuto = WirelessLANAuthCipher{Choice{Value: "auto", Label: "Auto"}} 73 | WirelessLANAuthCipherTkip = WirelessLANAuthCipher{Choice{Value: "tkip", Label: "TKIP"}} 74 | WirelessLANAuthCipherAes = WirelessLANAuthCipher{Choice{Value: "aes", Label: "AES"}} 75 | ) 76 | 77 | type WirelessLAN struct { 78 | NetboxObject 79 | // SSID is the name of the wireless lan. This field is required. 80 | SSID string `json:"ssid,omitempty"` 81 | // Vlan that the wireless lan is associated with. 82 | Vlan *Vlan `json:"vlan,omitempty"` 83 | // Group is the group of the wireless lan. 84 | Group *WirelessLANGroup `json:"group,omitempty"` 85 | // Status is the status of the wireless lan. This field is required. 86 | Status *WirelessLANStatus `json:"status,omitempty"` 87 | // Tenant of the wireless lan. 88 | Tenant *Tenant `json:"tenant,omitempty"` 89 | // AuthType is the authentication type of the wireless lan. 90 | AuthType *WirelessLANAuthType `json:"auth_type,omitempty"` 91 | // AuthCipher is the authentication cipher of the wireless lan. 92 | AuthCipher *WirelessLANAuthCipher `json:"auth_cipher,omitempty"` 93 | // AuthPsk is the pre-shared key of the wireless lan. 94 | AuthPsk string `json:"auth_psk,omitempty"` 95 | // Comments is the comments about the wireless lan. 96 | Comments string `json:"comments,omitempty"` 97 | } 98 | 99 | func (wl WirelessLAN) String() string { 100 | return fmt.Sprintf("WirelessLAN{SSID: %s}", wl.SSID) 101 | } 102 | 103 | // WirelessLAN implements IDItem interface. 104 | func (wl *WirelessLAN) GetID() int { 105 | return wl.ID 106 | } 107 | func (wl *WirelessLAN) GetObjectType() constants.ContentType { 108 | return constants.ContentTypeWirelessLAN 109 | } 110 | func (wl *WirelessLAN) GetAPIPath() constants.APIPath { 111 | return constants.WirelessLANsAPIPath 112 | } 113 | 114 | // WirelessLAN implements OrphanItem interface. 115 | func (wl *WirelessLAN) GetNetboxObject() *NetboxObject { 116 | return &wl.NetboxObject 117 | } 118 | -------------------------------------------------------------------------------- /internal/netbox/objects/wireless_test.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestWirelessLAN_String(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | wl WirelessLAN 12 | want string 13 | }{ 14 | { 15 | name: "Test string of a siimple WirelessLan", 16 | wl: WirelessLAN{ 17 | NetboxObject: NetboxObject{ 18 | ID: 1, 19 | CustomFields: map[string]interface{}{}, 20 | }, 21 | SSID: "Test", 22 | }, 23 | want: "WirelessLAN{SSID: Test}", 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := tt.wl.String(); got != tt.want { 29 | t.Errorf("WirelessLan.String() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestWirelessLANGroup_String(t *testing.T) { 36 | tests := []struct { 37 | name string 38 | wlg WirelessLANGroup 39 | want string 40 | }{ 41 | { 42 | name: "Test string of a simple WirelessLANGroup", 43 | wlg: WirelessLANGroup{ 44 | NetboxObject: NetboxObject{ 45 | ID: 1, 46 | CustomFields: map[string]interface{}{}, 47 | }, 48 | Name: "Test", 49 | Slug: "test", 50 | }, 51 | want: "WirelessLANGroup{Name: Test, Slug: test}", 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | if got := tt.wlg.String(); got != tt.want { 57 | t.Errorf("WirelessLANGroup.String() = %v, want %v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestWirelessLANGroup_GetID(t *testing.T) { 64 | tests := []struct { 65 | name string 66 | wlg *WirelessLANGroup 67 | want int 68 | }{ 69 | { 70 | name: "Test WirelessLANGroup get id", 71 | wlg: &WirelessLANGroup{ 72 | NetboxObject: NetboxObject{ 73 | ID: 1, 74 | }, 75 | }, 76 | want: 1, 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | if got := tt.wlg.GetID(); got != tt.want { 82 | t.Errorf("WirelessLANGroup.GetID() = %v, want %v", got, tt.want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestWirelessLANGroup_GetNetboxObject(t *testing.T) { 89 | tests := []struct { 90 | name string 91 | wlg *WirelessLANGroup 92 | want *NetboxObject 93 | }{ 94 | { 95 | name: "Test wlg get netbox object", 96 | wlg: &WirelessLANGroup{ 97 | NetboxObject: NetboxObject{ 98 | ID: 1, 99 | CustomFields: map[string]interface{}{ 100 | "x": "y", 101 | }, 102 | }, 103 | }, 104 | want: &NetboxObject{ 105 | ID: 1, 106 | CustomFields: map[string]interface{}{ 107 | "x": "y", 108 | }, 109 | }, 110 | }, 111 | } 112 | for _, tt := range tests { 113 | t.Run(tt.name, func(t *testing.T) { 114 | if got := tt.wlg.GetNetboxObject(); !reflect.DeepEqual(got, tt.want) { 115 | t.Errorf("WirelessLANGroup.GetNetboxObject() = %v, want %v", got, tt.want) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestWirelessLAN_GetID(t *testing.T) { 122 | tests := []struct { 123 | name string 124 | wl *WirelessLAN 125 | want int 126 | }{ 127 | { 128 | name: "Test Wireless LAN get id", 129 | wl: &WirelessLAN{ 130 | NetboxObject: NetboxObject{ 131 | ID: 1, 132 | }, 133 | }, 134 | want: 1, 135 | }, 136 | } 137 | for _, tt := range tests { 138 | t.Run(tt.name, func(t *testing.T) { 139 | if got := tt.wl.GetID(); got != tt.want { 140 | t.Errorf("WirelessLAN.GetID() = %v, want %v", got, tt.want) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func TestWirelessLAN_GetNetboxObject(t *testing.T) { 147 | tests := []struct { 148 | name string 149 | wl *WirelessLAN 150 | want *NetboxObject 151 | }{ 152 | { 153 | name: "Test wireless lan get netbox object", 154 | wl: &WirelessLAN{ 155 | NetboxObject: NetboxObject{ 156 | ID: 1, 157 | CustomFields: map[string]interface{}{ 158 | "x": "y", 159 | }, 160 | }, 161 | }, 162 | want: &NetboxObject{ 163 | ID: 1, 164 | CustomFields: map[string]interface{}{ 165 | "x": "y", 166 | }, 167 | }, 168 | }, 169 | } 170 | for _, tt := range tests { 171 | t.Run(tt.name, func(t *testing.T) { 172 | if got := tt.wl.GetNetboxObject(); !reflect.DeepEqual(got, tt.want) { 173 | t.Errorf("WirelessLAN.GetNetboxObject() = %v, want %v", got, tt.want) 174 | } 175 | }) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /internal/netbox/service/client.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/src-doo/netbox-ssot/internal/logger" 11 | "github.com/src-doo/netbox-ssot/internal/utils" 12 | ) 13 | 14 | // NetboxClient is a service used for communicating with the Netbox API. 15 | // It is created via constructor func newNetboxAPI(). 16 | type NetboxClient struct { 17 | Logger *logger.Logger 18 | HTTPClient *http.Client 19 | BaseURL string 20 | APIToken string 21 | Timeout int // in seconds 22 | MaxRetires int 23 | } 24 | 25 | // APIResponse is a struct that represents a response from the Netbox API. 26 | type APIResponse struct { 27 | StatusCode int 28 | Body []byte 29 | } 30 | 31 | // Constructor function for creating a new netBoxAPI instance. 32 | func NewNetboxClient( 33 | logger *logger.Logger, 34 | baseURL string, 35 | apiToken string, 36 | validateCert bool, 37 | timeout int, 38 | caCert string, 39 | ) (*NetboxClient, error) { 40 | httpClient, err := utils.NewHTTPClient(validateCert, caCert) 41 | if err != nil { 42 | return nil, fmt.Errorf("create new HTTP client: %s", err) 43 | } 44 | return &NetboxClient{ 45 | HTTPClient: httpClient, 46 | Logger: logger, 47 | BaseURL: baseURL, 48 | APIToken: apiToken, 49 | Timeout: timeout, 50 | }, nil 51 | } 52 | 53 | func (api *NetboxClient) doRequest( 54 | method string, 55 | path string, 56 | body io.Reader, 57 | ) (*APIResponse, error) { 58 | ctx, cancelCtx := context.WithTimeout( 59 | context.Background(), 60 | time.Second*time.Duration(api.Timeout), 61 | ) 62 | defer cancelCtx() 63 | 64 | req, err := http.NewRequestWithContext(ctx, method, api.BaseURL+path, body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // We add necessary headers to the request 70 | req.Header.Add("Authorization", "Token "+api.APIToken) 71 | req.Header.Add("Content-Type", "application/json") 72 | 73 | resp, err := api.HTTPClient.Do(req) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer resp.Body.Close() 78 | 79 | responseBody, err := io.ReadAll(resp.Body) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return &APIResponse{ 85 | StatusCode: resp.StatusCode, 86 | Body: responseBody, 87 | }, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/netbox/service/client_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "crypto/tls" 5 | "io" 6 | "log" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/src-doo/netbox-ssot/internal/constants" 12 | "github.com/src-doo/netbox-ssot/internal/logger" 13 | ) 14 | 15 | func TestNewNetBoxAPI(t *testing.T) { 16 | type args struct { 17 | logger *logger.Logger 18 | baseURL string 19 | apiToken string 20 | validateCert bool 21 | timeout int 22 | caCert string 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want *NetboxClient 28 | }{ 29 | { 30 | name: "test new API creation without ssl verify", 31 | args: args{ 32 | logger: &logger.Logger{Logger: log.Default()}, 33 | baseURL: "netbox.example.com", 34 | apiToken: "apitoken", 35 | validateCert: false, 36 | timeout: constants.DefaultAPITimeout, 37 | caCert: "", 38 | }, 39 | want: &NetboxClient{ 40 | Logger: &logger.Logger{Logger: log.Default()}, 41 | BaseURL: "netbox.example.com", 42 | APIToken: "apitoken", 43 | HTTPClient: &http.Client{ 44 | Transport: &http.Transport{ 45 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 46 | }, 47 | }, 48 | Timeout: constants.DefaultAPITimeout, 49 | }, 50 | }, 51 | { 52 | name: "test new API creation with ssl verify", 53 | args: args{ 54 | logger: &logger.Logger{Logger: log.Default()}, 55 | baseURL: "netbox.example.com", 56 | apiToken: "apitoken", 57 | validateCert: true, 58 | timeout: constants.DefaultAPITimeout, 59 | caCert: "", 60 | }, 61 | want: &NetboxClient{ 62 | Logger: &logger.Logger{Logger: log.Default()}, 63 | BaseURL: "netbox.example.com", 64 | APIToken: "apitoken", 65 | HTTPClient: &http.Client{Transport: &http.Transport{ 66 | TLSClientConfig: &tls.Config{}, 67 | }}, 68 | Timeout: constants.DefaultAPITimeout, 69 | }, 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | got, err := NewNetboxClient( 75 | tt.args.logger, 76 | tt.args.baseURL, 77 | tt.args.apiToken, 78 | tt.args.validateCert, 79 | tt.args.timeout, 80 | tt.args.caCert, 81 | ) 82 | if err != nil { 83 | t.Errorf("NewNetboxClient() error = %v", err) 84 | return 85 | } 86 | // Check non-pointer fields for simplicity or use an interface to mock clients 87 | if got.BaseURL != tt.want.BaseURL || got.APIToken != tt.want.APIToken || 88 | got.Timeout != tt.want.Timeout { 89 | t.Errorf("NewNetboxClient() got = %v, want %v", got, tt.want) 90 | } 91 | // Optionally check if HTTPClient is not nil to confirm it's initialized 92 | if got.HTTPClient == nil { 93 | t.Errorf("HTTPClient was not initialized") 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestNetboxAPI_doRequest(t *testing.T) { 100 | type args struct { 101 | method string 102 | path string 103 | body io.Reader 104 | } 105 | tests := []struct { 106 | name string 107 | netboxClient *NetboxClient 108 | args args 109 | want *APIResponse 110 | wantErr bool 111 | }{ 112 | { 113 | name: "Test GET /api/status/", 114 | netboxClient: MockNetboxClient, 115 | args: args{ 116 | method: http.MethodGet, 117 | path: "/api/status/", 118 | body: nil, 119 | }, 120 | want: &APIResponse{StatusCode: http.StatusOK, Body: []byte(MockVersionResponseJSON)}, 121 | wantErr: false, 122 | }, 123 | { 124 | name: "Test Invalid Request", 125 | netboxClient: MockNetboxClient, 126 | args: args{ 127 | method: "\n", // Invalid method 128 | path: "/api/status", 129 | body: nil, 130 | }, 131 | want: nil, 132 | wantErr: true, 133 | }, 134 | { 135 | name: "Client failure", 136 | netboxClient: FailingMockNetboxClient, 137 | args: args{ 138 | method: http.MethodGet, 139 | path: "/api/status", 140 | body: nil, 141 | }, 142 | want: nil, 143 | wantErr: true, 144 | }, 145 | { 146 | name: "Test ReadALL Error", 147 | netboxClient: MockNetboxClientWithReadError, 148 | args: args{ 149 | method: http.MethodGet, 150 | path: "/api/read-error", 151 | body: nil, 152 | }, 153 | want: nil, 154 | wantErr: true, 155 | }, 156 | } 157 | mockServer := CreateMockServer() 158 | defer mockServer.Close() 159 | MockNetboxClient.BaseURL = mockServer.URL 160 | for _, tt := range tests { 161 | t.Run(tt.name, func(t *testing.T) { 162 | got, err := tt.netboxClient.doRequest(tt.args.method, tt.args.path, tt.args.body) 163 | if (err != nil) != tt.wantErr { 164 | t.Errorf("NetboxAPI.doRequest() error = %v, wantErr %v", err, tt.wantErr) 165 | return 166 | } 167 | if !reflect.DeepEqual(got, tt.want) { 168 | t.Errorf("NetboxAPI.doRequest() = %v, want %v", got, tt.want) 169 | } 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /internal/netbox/service/rest_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/src-doo/netbox-ssot/internal/constants" 9 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 10 | ) 11 | 12 | func TestGetAll(t *testing.T) { 13 | type args struct { 14 | ctx context.Context 15 | api *NetboxClient 16 | extraParams string 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | want []objects.Tag 22 | wantErr bool 23 | }{ 24 | { 25 | name: "TestGetAll_Success", 26 | args: args{ 27 | ctx: context.WithValue( 28 | context.Background(), 29 | constants.CtxSourceKey, 30 | "test", 31 | ), 32 | api: MockNetboxClient, 33 | extraParams: "", 34 | }, 35 | // See predefined values in api_test for mockserver 36 | want: []objects.Tag{ 37 | { 38 | ID: 0, 39 | Name: "Source: proxmox", 40 | Slug: "source-proxmox", 41 | Color: "9e9e9e", 42 | Description: "Automatically created tag by netbox-ssot for source proxmox", 43 | }, 44 | { 45 | ID: 1, 46 | Name: "netbox-ssot", 47 | Slug: "netbox-ssot", 48 | Color: "00add8", 49 | Description: "Tag used by netbox-ssot to mark devices that are managed by it", 50 | }, 51 | }, 52 | wantErr: false, 53 | }, 54 | } 55 | for _, tt := range tests { 56 | mockServer := CreateMockServer() 57 | defer mockServer.Close() 58 | MockNetboxClient.BaseURL = mockServer.URL 59 | t.Run(tt.name, func(t *testing.T) { 60 | response, err := GetAll[objects.Tag](tt.args.ctx, tt.args.api, tt.args.extraParams) 61 | if (err != nil) != tt.wantErr { 62 | t.Errorf("GetAll() error = %v, wantErr %v", err, tt.wantErr) 63 | return 64 | } 65 | // Parse the object 66 | if !reflect.DeepEqual(response, tt.want) { 67 | t.Errorf("GetAll() = %v, want %v", response, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestPatch(t *testing.T) { 74 | type args struct { 75 | ctx context.Context 76 | api *NetboxClient 77 | objectID int 78 | body map[string]interface{} 79 | } 80 | tests := []struct { 81 | name string 82 | args args 83 | want *objects.Tag 84 | wantErr bool 85 | }{ 86 | { 87 | name: "Test patch tag", 88 | args: args{ 89 | ctx: context.WithValue(context.Background(), constants.CtxSourceKey, "test"), 90 | api: MockNetboxClient, 91 | objectID: 1, 92 | body: map[string]interface{}{ 93 | "description": "new description", 94 | }, 95 | }, 96 | // See predefined values in api_test for mockserver 97 | want: &MockTagPatchResponse, 98 | wantErr: false, 99 | }, 100 | } 101 | for _, tt := range tests { 102 | mockServer := CreateMockServer() 103 | defer mockServer.Close() 104 | MockNetboxClient.BaseURL = mockServer.URL 105 | t.Run(tt.name, func(t *testing.T) { 106 | response, err := Patch[objects.Tag]( 107 | tt.args.ctx, 108 | tt.args.api, 109 | tt.args.objectID, 110 | tt.args.body, 111 | ) 112 | if (err != nil) != tt.wantErr { 113 | t.Errorf("GetAll() error = %v, wantErr %v", err, tt.wantErr) 114 | return 115 | } 116 | if err != nil { 117 | t.Errorf("marshal tag patch response: %s", err) 118 | } 119 | if !reflect.DeepEqual(response, tt.want) { 120 | t.Errorf("Patch() = %v, want %v", response, tt.want) 121 | } 122 | }) 123 | } 124 | } 125 | 126 | func TestCreate(t *testing.T) { 127 | type args struct { 128 | ctx context.Context 129 | api *NetboxClient 130 | object *objects.Tag 131 | } 132 | tests := []struct { 133 | name string 134 | args args 135 | want *objects.Tag 136 | wantErr bool 137 | }{ 138 | { 139 | name: "Test create tag", 140 | args: args{ 141 | ctx: context.WithValue(context.Background(), constants.CtxSourceKey, "test"), 142 | api: MockNetboxClient, 143 | object: &MockTagCreateResponse, 144 | }, 145 | // See predefined values in api_test for mockserver 146 | want: &MockTagCreateResponse, 147 | wantErr: false, 148 | }, 149 | } 150 | for _, tt := range tests { 151 | mockServer := CreateMockServer() 152 | defer mockServer.Close() 153 | MockNetboxClient.BaseURL = mockServer.URL 154 | t.Run(tt.name, func(t *testing.T) { 155 | response, err := Create(tt.args.ctx, tt.args.api, tt.args.object) 156 | if (err != nil) != tt.wantErr { 157 | t.Errorf("GetAll() error = %v, wantErr %v", err, tt.wantErr) 158 | return 159 | } 160 | if !reflect.DeepEqual(response, tt.want) { 161 | t.Errorf("Patch() = %v, want %v", response, tt.want) 162 | } 163 | }) 164 | } 165 | } 166 | 167 | func TestBulkDeleteObjects(t *testing.T) { 168 | type args struct { 169 | ctx context.Context 170 | objectPath constants.APIPath 171 | idSet map[int]bool 172 | api *NetboxClient 173 | } 174 | tests := []struct { 175 | name string 176 | args args 177 | wantErr bool 178 | }{ 179 | { 180 | name: "Test bulk delete tags", 181 | args: args{ 182 | ctx: context.WithValue(context.Background(), constants.CtxSourceKey, "test"), 183 | objectPath: constants.TagsAPIPath, 184 | idSet: map[int]bool{0: true, 1: true}, 185 | api: MockNetboxClient, 186 | }, 187 | // See predefined values in api_test for mockserver 188 | wantErr: false, 189 | }, 190 | } 191 | for _, tt := range tests { 192 | mockServer := CreateMockServer() 193 | defer mockServer.Close() 194 | MockNetboxClient.BaseURL = mockServer.URL 195 | t.Run(tt.name, func(t *testing.T) { 196 | err := tt.args.api.BulkDeleteObjects(tt.args.ctx, tt.args.objectPath, tt.args.idSet) 197 | if (err != nil) != tt.wantErr { 198 | t.Errorf("GetAll() error = %v, wantErr %v", err, tt.wantErr) 199 | return 200 | } 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /internal/source/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | 7 | "github.com/src-doo/netbox-ssot/internal/logger" 8 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 9 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 10 | "github.com/src-doo/netbox-ssot/internal/parser" 11 | ) 12 | 13 | // Source is an interface for all sources (e.g. oVirt, VMware, etc.). 14 | type Source interface { 15 | // Init initializes the source 16 | Init() error 17 | // Sync syncs the source to Netbox inventory 18 | Sync(*inventory.NetboxInventory) error 19 | } 20 | 21 | // Config is a common configuration that all sources share. 22 | type Config struct { 23 | Logger *logger.Logger 24 | SourceConfig *parser.SourceConfig 25 | CustomCertPool *x509.CertPool 26 | SourceNameTag *objects.Tag 27 | SourceTypeTag *objects.Tag 28 | Ctx context.Context //nolint:containedctx 29 | CAFile string // path to the ca file 30 | } 31 | 32 | func (c Config) GetSourceTags() []*objects.Tag { 33 | return []*objects.Tag{c.SourceNameTag, c.SourceTypeTag} 34 | } 35 | -------------------------------------------------------------------------------- /internal/source/common/common_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | -------------------------------------------------------------------------------- /internal/source/dnac/dnac.go: -------------------------------------------------------------------------------- 1 | package dnac 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | "time" 8 | 9 | dnac "github.com/cisco-en-programmability/dnacenter-go-sdk/v7/sdk" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 11 | "github.com/src-doo/netbox-ssot/internal/source/common" 12 | "github.com/src-doo/netbox-ssot/internal/utils" 13 | ) 14 | 15 | //nolint:revive 16 | type DnacSource struct { 17 | common.Config 18 | 19 | // Dnac fetched data. Initialized in init functions. 20 | Sites map[string]dnac.ResponseSitesGetSiteResponse // SiteID -> Site 21 | Devices map[string]dnac.ResponseDevicesGetDeviceListResponse // DeviceID -> Device 22 | Interfaces map[string]dnac.ResponseDevicesGetAllInterfacesResponse // InterfaceID -> Interface 23 | Vlans map[int]dnac.ResponseDevicesGetDeviceInterfaceVLANsResponse // VlanID -> Vlan 24 | WirelessLANInterfaceName2VlanID map[string]int // InterfaceName -> VlanID 25 | SSID2WirelessProfileDetails map[string]dnac.ResponseItemWirelessGetWirelessProfileProfileDetailsSSIDDetails 26 | // SSID2WlanGroupName SSID -> WirelessLANGroup name 27 | SSID2WlanGroupName map[string]string 28 | // SSID2SecurityDetails WirelessLANName -> SSIDDetails 29 | SSID2SecurityDetails map[string]dnac.ResponseItemWirelessGetEnterpriseSSIDSSIDDetails 30 | 31 | // Relations between dnac data. Initialized in init functions. 32 | Site2Parent map[string]string // Site ID -> Parent Site ID 33 | Site2Devices map[string]map[string]bool // Site ID - > set of device IDs 34 | Device2Site map[string]string // Device ID -> Site ID 35 | DeviceID2InterfaceIDs map[string][]string // DeviceID -> []InterfaceID 36 | 37 | // Netbox related data for easier access. Initialized in sync functions. 38 | // DeviceID2isMissingPrimaryIP stores devices without primary IP. See ds.syncMissingDevicePrimaryIPs 39 | DeviceID2isMissingPrimaryIP sync.Map 40 | // VID2nbVlan: VlanID -> nbVlan 41 | VID2nbVlan sync.Map 42 | // SiteID2nbSite: SiteID -> nbSite 43 | SiteID2nbSite sync.Map 44 | DeviceID2nbDevice sync.Map // DeviceID -> nbDevice 45 | InterfaceID2nbInterface sync.Map // InterfaceID -> nbInterface 46 | } 47 | 48 | func (ds *DnacSource) Init() error { 49 | dnacURL := fmt.Sprintf( 50 | "%s://%s:%d", 51 | ds.Config.SourceConfig.HTTPScheme, 52 | ds.Config.SourceConfig.Hostname, 53 | ds.Config.SourceConfig.Port, 54 | ) 55 | Client, err := dnac.NewClientWithOptions( 56 | dnacURL, 57 | ds.SourceConfig.Username, 58 | ds.SourceConfig.Password, 59 | "false", 60 | strconv.FormatBool(ds.SourceConfig.ValidateCert), 61 | nil, 62 | ) 63 | if err != nil { 64 | return fmt.Errorf("creating dnac client: %s", err) 65 | } 66 | // Initialize items from vsphere API to local storage 67 | initFunctions := []func(*dnac.Client) error{ 68 | ds.initSites, 69 | ds.initMemberships, 70 | ds.initDevices, 71 | ds.initInterfaces, 72 | ds.initWirelessLANs, 73 | } 74 | 75 | for _, initFunc := range initFunctions { 76 | startTime := time.Now() 77 | if err := initFunc(Client); err != nil { 78 | return fmt.Errorf("dnac initialization failure: %v", err) 79 | } 80 | duration := time.Since(startTime) 81 | ds.Logger.Infof( 82 | ds.Ctx, 83 | "Successfully initialized %s in %f seconds", 84 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 85 | duration.Seconds(), 86 | ) 87 | } 88 | return nil 89 | } 90 | 91 | func (ds *DnacSource) Sync(nbi *inventory.NetboxInventory) error { 92 | syncFunctions := []func(*inventory.NetboxInventory) error{ 93 | ds.syncSites, 94 | ds.syncVlans, 95 | ds.syncDevices, 96 | ds.syncDeviceInterfaces, 97 | ds.syncWirelessLANs, 98 | ds.syncMissingDevicePrimaryIPs, 99 | } 100 | 101 | for _, syncFunc := range syncFunctions { 102 | startTime := time.Now() 103 | err := syncFunc(nbi) 104 | if err != nil { 105 | return err 106 | } 107 | duration := time.Since(startTime) 108 | ds.Logger.Infof( 109 | ds.Ctx, 110 | "Successfully synced %s in %f seconds", 111 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 112 | duration.Seconds(), 113 | ) 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/source/dnac/dnac_test.go: -------------------------------------------------------------------------------- 1 | package dnac 2 | -------------------------------------------------------------------------------- /internal/source/fmc/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/src-doo/netbox-ssot/internal/constants" 10 | "github.com/src-doo/netbox-ssot/internal/logger" 11 | ) 12 | 13 | type FMCClient struct { 14 | HTTPClient *http.Client 15 | BaseURL string 16 | Username string 17 | Password string 18 | AccessToken string 19 | RefreshToken string 20 | DefaultTimeout time.Duration 21 | Logger *logger.Logger 22 | Ctx context.Context 23 | } 24 | 25 | // NewFMCClient creates a new FMC client with the given parameters. 26 | // It authenticates to the FMC API and stores the access and refresh tokens. 27 | func NewFMCClient( 28 | context context.Context, 29 | username string, 30 | password string, 31 | httpScheme string, 32 | hostname string, 33 | port int, 34 | httpClient *http.Client, 35 | logger *logger.Logger, 36 | ) (*FMCClient, error) { 37 | c := &FMCClient{ 38 | HTTPClient: httpClient, 39 | BaseURL: fmt.Sprintf("%s://%s:%d/api", httpScheme, hostname, port), 40 | Username: username, 41 | Password: password, 42 | DefaultTimeout: time.Second * constants.DefaultAPITimeout, 43 | Logger: logger, 44 | Ctx: context, 45 | } 46 | 47 | aToken, rToken, err := c.Authenticate() 48 | if err != nil { 49 | return nil, fmt.Errorf("authentication: %w", err) 50 | } 51 | 52 | c.AccessToken = aToken 53 | c.RefreshToken = rToken 54 | 55 | return c, nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/source/fmc/fmc.go: -------------------------------------------------------------------------------- 1 | package fmc 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 8 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 9 | "github.com/src-doo/netbox-ssot/internal/source/common" 10 | "github.com/src-doo/netbox-ssot/internal/source/fmc/client" 11 | "github.com/src-doo/netbox-ssot/internal/utils" 12 | ) 13 | 14 | // FMCSource represents Cisco Firewall Management Center. 15 | // 16 | //nolint:revive 17 | type FMCSource struct { 18 | common.Config 19 | 20 | // FMC data. Initialized in init functions. 21 | // Domains is a map of domain UUIDs to Domain objects. 22 | Domains map[string]client.Domain 23 | // Devices is a map of device IDs to DeviceInfo objects. 24 | Devices map[string]*client.DeviceInfo 25 | // DevicePhysicalIfaces is a map of device IDs to a slice of PhysicalInterfaceInfo objects. 26 | DevicePhysicalIfaces map[string][]*client.PhysicalInterfaceInfo 27 | // DeviceVlanIfaces is a map of device IDs to a slice of VLANInterfaceInfo objects. 28 | DeviceVlanIfaces map[string][]*client.VLANInterfaceInfo 29 | // DeviceEtherChannelIfaces is a map of device IDs to a slice of EtherChannelInterfaceInfo objects. 30 | DeviceEtherChannelIfaces map[string][]*client.EtherChannelInterfaceInfo 31 | // DeviceSubIfaces is a map of device IDs to a slice of SubInterfaceInfo objects. 32 | DeviceSubIfaces map[string][]*client.SubInterfaceInfo 33 | 34 | // Netbox devices representing firewalls. 35 | NBDevices map[string]*objects.Device 36 | // NBInterfaces represents all fmc interfaces that have been synced to netbox. 37 | // It is a map of interface name to interface, so we can find parents of sub interfaces. 38 | Name2NBInterface map[string]*objects.Interface 39 | } 40 | 41 | func (fmcs *FMCSource) Init() error { 42 | httpClient, err := utils.NewHTTPClient(fmcs.SourceConfig.ValidateCert, fmcs.CAFile) 43 | if err != nil { 44 | return fmt.Errorf("create new http client: %s", err) 45 | } 46 | 47 | c, err := client.NewFMCClient( 48 | fmcs.Ctx, 49 | fmcs.SourceConfig.Username, 50 | fmcs.SourceConfig.Password, 51 | string(fmcs.SourceConfig.HTTPScheme), 52 | fmcs.SourceConfig.Hostname, 53 | fmcs.SourceConfig.Port, 54 | httpClient, 55 | fmcs.Logger, 56 | ) 57 | if err != nil { 58 | return fmt.Errorf("create FMC client: %s", err) 59 | } 60 | 61 | fmcs.Name2NBInterface = make(map[string]*objects.Interface) 62 | 63 | initFunctions := []func(*client.FMCClient) error{ 64 | fmcs.initObjects, 65 | } 66 | for _, initFunc := range initFunctions { 67 | startTime := time.Now() 68 | if err := initFunc(c); err != nil { 69 | return fmt.Errorf("fmc initialization failure: %v", err) 70 | } 71 | duration := time.Since(startTime) 72 | fmcs.Logger.Infof( 73 | fmcs.Ctx, 74 | "Successfully initialized %s in %f seconds", 75 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 76 | duration.Seconds(), 77 | ) 78 | } 79 | return nil 80 | } 81 | 82 | func (fmcs *FMCSource) Sync(nbi *inventory.NetboxInventory) error { 83 | syncFunctions := []func(*inventory.NetboxInventory) error{ 84 | fmcs.syncDevices, 85 | } 86 | 87 | for _, syncFunc := range syncFunctions { 88 | startTime := time.Now() 89 | err := syncFunc(nbi) 90 | if err != nil { 91 | return err 92 | } 93 | duration := time.Since(startTime) 94 | fmcs.Logger.Infof( 95 | fmcs.Ctx, 96 | "Successfully synced %s in %f seconds", 97 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 98 | duration.Seconds(), 99 | ) 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/source/fmc/fmc_test.go: -------------------------------------------------------------------------------- 1 | package fmc 2 | -------------------------------------------------------------------------------- /internal/source/fortigate/fortigate.go: -------------------------------------------------------------------------------- 1 | package fortigate 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 11 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 12 | "github.com/src-doo/netbox-ssot/internal/source/common" 13 | "github.com/src-doo/netbox-ssot/internal/utils" 14 | ) 15 | 16 | //nolint:revive 17 | type FortigateSource struct { 18 | common.Config 19 | // Fortinet data. Initialized in init functions. 20 | SystemInfo FortiSystemInfo // Map storing system information 21 | Ifaces map[string]InterfaceResponse // iface name -> FortigateInterface 22 | 23 | // NBFirewall representing fortinet firewall created in syncDevice func. 24 | NBFirewall *objects.Device 25 | } 26 | 27 | type FortiSystemInfo struct { 28 | Hostname string 29 | Version string 30 | Serial string 31 | } 32 | 33 | type FortiClient struct { 34 | HTTPClient *http.Client 35 | BaseURL string 36 | APIToken string 37 | } 38 | 39 | func NewAPIClient(apiToken string, baseURL string, httpClient *http.Client) *FortiClient { 40 | return &FortiClient{ 41 | HTTPClient: httpClient, 42 | BaseURL: baseURL, 43 | APIToken: apiToken, 44 | } 45 | } 46 | 47 | func (c FortiClient) MakeRequest( 48 | ctx context.Context, 49 | method, path string, 50 | body io.Reader, 51 | ) (*http.Response, error) { 52 | req, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s/%s", c.BaseURL, path), body) 53 | if err != nil { 54 | return nil, err 55 | } 56 | // Set the Authorization header. 57 | req.Header.Set("Authorization", "Bearer "+c.APIToken) 58 | return c.HTTPClient.Do(req) 59 | } 60 | 61 | func (fs *FortigateSource) Init() error { 62 | httpClient, err := utils.NewHTTPClient(fs.SourceConfig.ValidateCert, fs.CAFile) 63 | if err != nil { 64 | return fmt.Errorf("create new http client: %s", err) 65 | } 66 | c := NewAPIClient( 67 | fs.SourceConfig.APIToken, 68 | fmt.Sprintf( 69 | "%s://%s:%d/api/v2", 70 | fs.SourceConfig.HTTPScheme, 71 | fs.SourceConfig.Hostname, 72 | fs.SourceConfig.Port, 73 | ), 74 | httpClient, 75 | ) 76 | ctx := context.Background() 77 | defer ctx.Done() 78 | 79 | initFunctions := []func(context.Context, *FortiClient) error{ 80 | fs.initSystemInfo, 81 | fs.initInterfaces, 82 | } 83 | for _, initFunc := range initFunctions { 84 | startTime := time.Now() 85 | if err := initFunc(ctx, c); err != nil { 86 | return fmt.Errorf("fortigate initialization failure: %v", err) 87 | } 88 | duration := time.Since(startTime) 89 | fs.Logger.Infof( 90 | fs.Ctx, 91 | "Successfully initialized %s in %f seconds", 92 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 93 | duration.Seconds(), 94 | ) 95 | } 96 | return nil 97 | } 98 | 99 | func (fs *FortigateSource) Sync(nbi *inventory.NetboxInventory) error { 100 | syncFunctions := []func(*inventory.NetboxInventory) error{ 101 | fs.syncDevice, 102 | fs.syncInterfaces, 103 | } 104 | 105 | for _, syncFunc := range syncFunctions { 106 | startTime := time.Now() 107 | err := syncFunc(nbi) 108 | if err != nil { 109 | return err 110 | } 111 | duration := time.Since(startTime) 112 | fs.Logger.Infof( 113 | fs.Ctx, 114 | "Successfully synced %s in %f seconds", 115 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 116 | duration.Seconds(), 117 | ) 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /internal/source/fortigate/fortigate_init.go: -------------------------------------------------------------------------------- 1 | package fortigate 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type APIResponse[T any] struct { 12 | HTTPStatus int `json:"http_status"` 13 | Serial string `json:"serial"` 14 | Version string `json:"version"` 15 | Results T `json:"results"` 16 | } 17 | 18 | type DeviceResponse struct { 19 | Hostname string `json:"hostname"` 20 | } 21 | 22 | type InterfaceResponse struct { 23 | Name string `json:"name"` 24 | Vdom string `json:"vdom"` 25 | IP string `json:"Ip"` 26 | Type string `json:"type"` 27 | Status string `json:"status"` 28 | Speed string `json:"speed"` 29 | Description string `json:"description"` 30 | MTU int `json:"mtu"` 31 | MAC string `json:"macaddr"` 32 | VlanID int `json:"vlanid"` 33 | SecondaryIP []SecondaryIP `json:"secondaryip"` 34 | VRRPIP []VRRPIP `json:"vrrp"` 35 | } 36 | 37 | type SecondaryIP struct { 38 | IP string `json:"ip"` 39 | } 40 | type VRRPIP struct { 41 | VRIP string `json:"vrip"` 42 | } 43 | 44 | // Init system info collects system info from paloalto. 45 | func (fs *FortigateSource) initSystemInfo(ctx context.Context, c *FortiClient) error { 46 | res, err := c.MakeRequest(ctx, http.MethodGet, "cmdb/system/global/", nil) 47 | if err != nil { 48 | return fmt.Errorf("request error: %s", err) 49 | } 50 | defer res.Body.Close() 51 | 52 | body, err := io.ReadAll(res.Body) 53 | if err != nil { 54 | return fmt.Errorf("body read error: %s", err) 55 | } 56 | 57 | var deviceResponse APIResponse[DeviceResponse] 58 | err = json.Unmarshal(body, &deviceResponse) 59 | if err != nil { 60 | return fmt.Errorf("body unmarshal error: %s", err) 61 | } 62 | 63 | if deviceResponse.HTTPStatus != http.StatusOK { 64 | return fmt.Errorf("got http status: %d", deviceResponse.HTTPStatus) 65 | } 66 | 67 | fs.SystemInfo = FortiSystemInfo{ 68 | Hostname: deviceResponse.Results.Hostname, 69 | Version: deviceResponse.Version, 70 | Serial: deviceResponse.Serial, 71 | } 72 | 73 | return nil 74 | } 75 | 76 | // Fetches all information about interfaces from fortigate api. 77 | func (fs *FortigateSource) initInterfaces(ctx context.Context, c *FortiClient) error { 78 | // Interfaces 79 | res, err := c.MakeRequest(ctx, http.MethodGet, "cmdb/system/interface/", nil) 80 | if err != nil { 81 | return fmt.Errorf("request error: %s", err) 82 | } 83 | defer res.Body.Close() 84 | body, err := io.ReadAll(res.Body) 85 | if err != nil { 86 | return fmt.Errorf("body read error: %s", err) 87 | } 88 | var interfaceResponse APIResponse[[]InterfaceResponse] 89 | err = json.Unmarshal(body, &interfaceResponse) 90 | if err != nil { 91 | return fmt.Errorf("body unamrshal error: %s", err) 92 | } 93 | 94 | if interfaceResponse.HTTPStatus != http.StatusOK { 95 | return fmt.Errorf("got http status: %d", interfaceResponse.HTTPStatus) 96 | } 97 | 98 | fs.Ifaces = make(map[string]InterfaceResponse, len(interfaceResponse.Results)) 99 | for _, iface := range interfaceResponse.Results { 100 | fs.Ifaces[iface.Name] = iface 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/source/fortigate/fortigate_test.go: -------------------------------------------------------------------------------- 1 | package fortigate 2 | -------------------------------------------------------------------------------- /internal/source/ios-xe/iosxe.go: -------------------------------------------------------------------------------- 1 | package iosxe 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/scrapli/scrapligo/driver/netconf" 8 | "github.com/scrapli/scrapligo/driver/options" 9 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 11 | "github.com/src-doo/netbox-ssot/internal/source/common" 12 | "github.com/src-doo/netbox-ssot/internal/utils" 13 | ) 14 | 15 | //nolint:revive 16 | type IOSXESource struct { 17 | common.Config 18 | 19 | // IOSXE fetched data. Initialized in init functions. 20 | HardwareInfo hardwareReply 21 | SystemInfo systemReply 22 | Interfaces map[string]iface 23 | ArpEntries []arpEntry 24 | 25 | // IOSXE synced data. Created in sync functions. 26 | NBDevice *objects.Device 27 | NBInterfaces map[string]*objects.Interface // interfaceName -> netboxInterface 28 | } 29 | 30 | func (is *IOSXESource) Init() error { 31 | d, err := netconf.NewDriver( 32 | is.SourceConfig.Hostname, 33 | options.WithAuthUsername(is.SourceConfig.Username), 34 | options.WithAuthPassword(is.SourceConfig.Password), 35 | options.WithPort(is.SourceConfig.Port), 36 | options.WithAuthNoStrictKey(), 37 | // See https://github.com/SRC-doo/netbox-ssot/issues/498 38 | options.WithSSHConfigFile("~/.ssh/config"), 39 | ) 40 | if err != nil { 41 | return fmt.Errorf("failed to create driver: %s", err) 42 | } 43 | err = d.Open() 44 | if err != nil { 45 | return fmt.Errorf("failed to open driver: %s", err) 46 | } 47 | defer d.Close() 48 | 49 | // Initialize items from vsphere API to local storage 50 | initFunctions := []func(*netconf.Driver) error{ 51 | is.initDeviceInfo, 52 | is.initDeviceHardwareInfo, 53 | is.initInterfaces, 54 | is.initArpData, 55 | } 56 | 57 | for _, initFunc := range initFunctions { 58 | startTime := time.Now() 59 | if err := initFunc(d); err != nil { 60 | return fmt.Errorf("iosxe initialization failure: %v", err) 61 | } 62 | duration := time.Since(startTime) 63 | is.Logger.Infof( 64 | is.Ctx, 65 | "Successfully initialized %s in %f seconds", 66 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 67 | duration.Seconds(), 68 | ) 69 | } 70 | return nil 71 | } 72 | 73 | func (is *IOSXESource) Sync(nbi *inventory.NetboxInventory) error { 74 | syncFunctions := []func(*inventory.NetboxInventory) error{ 75 | is.syncDevice, 76 | is.syncInterfaces, 77 | is.syncArpTable, 78 | } 79 | 80 | for _, syncFunc := range syncFunctions { 81 | startTime := time.Now() 82 | err := syncFunc(nbi) 83 | if err != nil { 84 | return err 85 | } 86 | duration := time.Since(startTime) 87 | is.Logger.Infof( 88 | is.Ctx, 89 | "Successfully synced %s in %f seconds", 90 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 91 | duration.Seconds(), 92 | ) 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/source/ios-xe/iosxe_filters.go: -------------------------------------------------------------------------------- 1 | package iosxe 2 | 3 | const hwFilter = `<device-hardware-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-device-hardware-oper"> 4 | <device-hardware> 5 | <device-inventory/> 6 | </device-hardware> 7 | </device-hardware-data>` 8 | 9 | const systemFilter = `<system xmlns="http://openconfig.net/yang/system"> 10 | <config> 11 | </config> 12 | <state> 13 | <hostname/> 14 | <domain-name/> 15 | </state> 16 | </system> 17 | ` 18 | 19 | const interfaceFilter = `<interfaces xmlns="http://openconfig.net/yang/interfaces"> 20 | <interface> 21 | <name/> 22 | <state> 23 | <description/> 24 | <name/> 25 | <type/> 26 | <enabled/> 27 | </state> 28 | <ethernet xmlns="http://openconfig.net/yang/interfaces/ethernet"> 29 | <state> 30 | <mac-address/> 31 | <auto-negotiate/> 32 | <port-speed/> 33 | </state> 34 | </ethernet> 35 | </interface> 36 | </interfaces>` 37 | 38 | const arpFilter = `<arp-data xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-arp-oper"/>` 39 | -------------------------------------------------------------------------------- /internal/source/ios-xe/iosxe_init.go: -------------------------------------------------------------------------------- 1 | package iosxe 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | 7 | "github.com/scrapli/scrapligo/driver/netconf" 8 | ) 9 | 10 | func (is *IOSXESource) initDeviceInfo(d *netconf.Driver) error { 11 | r, err := d.Get(systemFilter) 12 | if err != nil { 13 | return fmt.Errorf("error with system filter: %s", err) 14 | } 15 | err = xml.Unmarshal(r.RawResult, &is.SystemInfo) 16 | if err != nil { 17 | return fmt.Errorf("error with unmarshaling device info: %s", err) 18 | } 19 | return nil 20 | } 21 | 22 | func (is *IOSXESource) initDeviceHardwareInfo(d *netconf.Driver) error { 23 | r, err := d.Get(hwFilter) 24 | if err != nil { 25 | return fmt.Errorf("error with hardware filter: %s", err) 26 | } 27 | err = xml.Unmarshal(r.RawResult, &is.HardwareInfo) 28 | if err != nil { 29 | return fmt.Errorf("error with unmarshaling hardware info: %s", err) 30 | } 31 | return nil 32 | } 33 | 34 | func (is *IOSXESource) initInterfaces(d *netconf.Driver) error { 35 | var ifaceReply interfaceReply 36 | r, err := d.Get(interfaceFilter) 37 | if err != nil { 38 | return fmt.Errorf("error with interface filter: %s", err) 39 | } 40 | err = xml.Unmarshal(r.RawResult, &ifaceReply) 41 | if err != nil { 42 | return fmt.Errorf("error with unmarshaling interfaces: %s", err) 43 | } 44 | is.Interfaces = make(map[string]iface) 45 | for _, iface := range ifaceReply.Interfaces { 46 | is.Interfaces[iface.Name] = iface 47 | } 48 | return nil 49 | } 50 | 51 | func (is *IOSXESource) initArpData(d *netconf.Driver) error { 52 | var arpReply arpReply 53 | r, err := d.Get(arpFilter) 54 | if err != nil { 55 | return fmt.Errorf("error with arp filter: %s", err) 56 | } 57 | err = xml.Unmarshal(r.RawResult, &arpReply) 58 | if err != nil { 59 | return fmt.Errorf("error with unmarshaling arp reply: %s", err) 60 | } 61 | is.ArpEntries = make([]arpEntry, 0) 62 | for _, arpVrf := range arpReply.ArpVrf { 63 | is.ArpEntries = append(is.ArpEntries, arpVrf.ArpOper...) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/source/ios-xe/iosxe_schemas.go: -------------------------------------------------------------------------------- 1 | package iosxe 2 | 3 | import "encoding/xml" 4 | 5 | // hardwareReply is the top-level structure for the hardware response. 6 | type hardwareReply struct { 7 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"` 8 | MessageID string `xml:"message-id,attr"` 9 | Inventory []HWInventory `xml:"data>device-hardware-data>device-hardware>device-inventory"` 10 | } 11 | 12 | // HWInventory holds the part and serial numbers from each inventory entry. 13 | type HWInventory struct { 14 | Type string `xml:"hw-type"` 15 | DevIndex string `xml:"hw-dev-index"` 16 | Version string `xml:"version"` 17 | PartNumber string `xml:"part-number"` 18 | SerialNumber string `xml:"serial-number"` 19 | Description string `xml:"hw-description"` 20 | DevName string `xml:"dev-name"` 21 | Class string `xml:"hw-class"` 22 | } 23 | 24 | type systemReply struct { 25 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"` 26 | MessageID string `xml:"message-id,attr"` 27 | Hostname string `xml:"data>system>state>hostname"` 28 | DomainName string `xml:"data>system>state>domain-name"` 29 | } 30 | 31 | // InterfacesReply holds the entire response structure with the message ID and a slice of interfaces. 32 | type interfaceReply struct { 33 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"` 34 | MessageID string `xml:"message-id,attr"` 35 | Interfaces []iface `xml:"data>interfaces>interface"` 36 | } 37 | 38 | type iface struct { 39 | Name string `xml:"name"` 40 | State interfaceState `xml:"state"` 41 | Ethernet ethernetState `xml:"ethernet>state"` 42 | } 43 | 44 | // InterfaceState captures the state of the interface, including its operational status. 45 | type interfaceState struct { 46 | Name string `xml:"name"` 47 | Type string `xml:"type,attr"` 48 | Enabled bool `xml:"enabled"` 49 | Description string `xml:"description"` 50 | } 51 | 52 | // EthernetState provides details about the Ethernet settings. 53 | type ethernetState struct { 54 | MACAddress string `xml:"mac-address"` 55 | AutoNegotiate bool `xml:"auto-negotiate"` 56 | PortSpeed string `xml:"port-speed"` 57 | } 58 | 59 | type arpReply struct { 60 | XMLName xml.Name `xml:"urn:ietf:params:xml:ns:netconf:base:1.0 rpc-reply"` 61 | MessageID string `xml:"message-id,attr"` 62 | ArpVrf []arpVrf `xml:"data>arp-data>arp-vrf"` 63 | } 64 | 65 | type arpVrf struct { 66 | Vrf string `xml:"vrf"` 67 | ArpOper []arpEntry `xml:"arp-oper"` 68 | } 69 | 70 | type arpEntry struct { 71 | Address string `xml:"address"` 72 | Interface string `xml:"interface"` 73 | Type string `xml:"type"` 74 | Mode string `xml:"mode"` 75 | HWType string `xml:"hwtype"` 76 | MAC string `xml:"hardware"` 77 | } 78 | -------------------------------------------------------------------------------- /internal/source/ios-xe/iosxe_test.go: -------------------------------------------------------------------------------- 1 | package iosxe 2 | -------------------------------------------------------------------------------- /internal/source/ovirt/ovirt.go: -------------------------------------------------------------------------------- 1 | package ovirt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | ovirtsdk4 "github.com/ovirt/go-ovirt" 9 | "github.com/src-doo/netbox-ssot/internal/constants" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 11 | "github.com/src-doo/netbox-ssot/internal/source/common" 12 | "github.com/src-doo/netbox-ssot/internal/utils" 13 | ) 14 | 15 | // OVirtSource represents an oVirt source. 16 | // 17 | //nolint:revive 18 | type OVirtSource struct { 19 | common.Config 20 | Disks map[string]*ovirtsdk4.Disk 21 | DataCenters map[string]*ovirtsdk4.DataCenter 22 | Clusters map[string]*ovirtsdk4.Cluster 23 | Hosts map[string]*ovirtsdk4.Host 24 | Vms map[string]*ovirtsdk4.Vm 25 | Networks *NetworkData 26 | } 27 | 28 | type NetworkData struct { 29 | OVirtNetworks map[string]*ovirtsdk4.Network 30 | VnicProfile2Network map[string]string // vnicProfileId -> networkId 31 | Vid2Name map[int]string 32 | } 33 | 34 | // Function that initializes state from ovirt api to local storage. 35 | func (o *OVirtSource) Init() error { 36 | // Build the connection 37 | o.Logger.Debug(o.Ctx, "Initializing oVirt source ", o.SourceConfig.Name) 38 | connBuilder := ovirtsdk4.NewConnectionBuilder(). 39 | URL(fmt.Sprintf( 40 | "%s://%s:%d/ovirt-engine/api", 41 | o.SourceConfig.HTTPScheme, 42 | o.SourceConfig.Hostname, 43 | o.SourceConfig.Port, 44 | ), 45 | ). 46 | Username(o.SourceConfig.Username). 47 | Password(o.SourceConfig.Password). 48 | Insecure(!o.SourceConfig.ValidateCert). 49 | Compress(true). 50 | Timeout(time.Second * constants.DefaultAPITimeout). 51 | CAFile(o.Config.CAFile) 52 | 53 | if o.Config.CAFile != "" { 54 | connBuilder.CAFile(o.Config.CAFile) 55 | } 56 | 57 | // Initialize the connection 58 | conn, err := connBuilder.Build() 59 | if err != nil { 60 | return fmt.Errorf("failed to create oVirt connection: %v", err) 61 | } 62 | defer conn.Close() 63 | 64 | // Initialize items to local storage 65 | initFunctions := []func(*ovirtsdk4.Connection) error{ 66 | o.initNetworks, 67 | o.initDisks, 68 | o.initDataCenters, 69 | o.initClusters, 70 | o.initHosts, 71 | o.initVms, 72 | } 73 | 74 | for _, initFunc := range initFunctions { 75 | startTime := time.Now() 76 | if err := initFunc(conn); err != nil { 77 | return fmt.Errorf( 78 | "failed to initialize oVirt %s: %v", 79 | strings.TrimPrefix(fmt.Sprintf("%T", initFunc), "*source.OVirtSource.Init"), 80 | err, 81 | ) 82 | } 83 | duration := time.Since(startTime) 84 | o.Logger.Infof( 85 | o.Ctx, 86 | "Successfully initialized %s in %f seconds", 87 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 88 | duration.Seconds(), 89 | ) 90 | } 91 | return nil 92 | } 93 | 94 | // Function that syncs all data from oVirt to Netbox. 95 | func (o *OVirtSource) Sync(nbi *inventory.NetboxInventory) error { 96 | syncFunctions := []func(*inventory.NetboxInventory) error{ 97 | o.syncNetworks, 98 | o.syncDatacenters, 99 | o.syncClusters, 100 | o.syncHosts, 101 | o.syncVMs, 102 | } 103 | for _, syncFunc := range syncFunctions { 104 | startTime := time.Now() 105 | err := syncFunc(nbi) 106 | if err != nil { 107 | return err 108 | } 109 | duration := time.Since(startTime) 110 | o.Logger.Infof( 111 | o.Ctx, 112 | "Successfully synced %s in %f seconds", 113 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 114 | duration.Seconds(), 115 | ) 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/source/ovirt/ovirt_init.go: -------------------------------------------------------------------------------- 1 | package ovirt 2 | 3 | import ( 4 | "fmt" 5 | 6 | ovirtsdk4 "github.com/ovirt/go-ovirt" 7 | ) 8 | 9 | // Fetches networks from ovirt api and stores them to local object. 10 | func (o *OVirtSource) initNetworks(conn *ovirtsdk4.Connection) error { 11 | networksResponse, err := conn.SystemService(). 12 | NetworksService(). 13 | List(). 14 | Follow("vnicprofiles"). 15 | Send() 16 | if err != nil { 17 | return fmt.Errorf("init oVirt networks: %v", err) 18 | } 19 | o.Networks = &NetworkData{ 20 | OVirtNetworks: make(map[string]*ovirtsdk4.Network), 21 | Vid2Name: make(map[int]string), 22 | VnicProfile2Network: make(map[string]string), 23 | } 24 | if networks, ok := networksResponse.Networks(); ok { 25 | for _, network := range networks.Slice() { 26 | if networkID, ok := network.Id(); ok { 27 | o.Networks.OVirtNetworks[networkID] = network 28 | if vlan, exists := network.Vlan(); exists { 29 | if vlanID, exists := vlan.Id(); exists { 30 | o.Networks.Vid2Name[int(vlanID)] = network.MustName() 31 | } 32 | } 33 | if vnicProfiles, ok := network.VnicProfiles(); ok { 34 | for _, vnicProfile := range vnicProfiles.Slice() { 35 | if vnicProfileID, ok := vnicProfile.Id(); ok { 36 | o.Networks.VnicProfile2Network[vnicProfileID] = networkID 37 | } 38 | } 39 | } 40 | } 41 | } 42 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt networks: ", o.Networks) 43 | } else { 44 | o.Logger.Warning(o.Ctx, "Error initializing oVirt networks") 45 | } 46 | return nil 47 | } 48 | 49 | func (o *OVirtSource) initDisks(conn *ovirtsdk4.Connection) error { 50 | // Get the disks 51 | disksResponse, err := conn.SystemService().DisksService().List().Send() 52 | if err != nil { 53 | return fmt.Errorf("failed to get oVirt disks: %v", err) 54 | } 55 | o.Disks = make(map[string]*ovirtsdk4.Disk) 56 | if disks, ok := disksResponse.Disks(); ok { 57 | for _, disk := range disks.Slice() { 58 | o.Disks[disk.MustId()] = disk 59 | } 60 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt disks: ", o.Disks) 61 | } else { 62 | o.Logger.Warning(o.Ctx, "Error initializing oVirt disks") 63 | } 64 | return nil 65 | } 66 | 67 | func (o *OVirtSource) initDataCenters(conn *ovirtsdk4.Connection) error { 68 | dataCentersResponse, err := conn.SystemService().DataCentersService().List().Send() 69 | if err != nil { 70 | return fmt.Errorf("failed to get oVirt data centers: %v", err) 71 | } 72 | o.DataCenters = make(map[string]*ovirtsdk4.DataCenter) 73 | if dataCenters, ok := dataCentersResponse.DataCenters(); ok { 74 | for _, dataCenter := range dataCenters.Slice() { 75 | o.DataCenters[dataCenter.MustId()] = dataCenter 76 | } 77 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt data centers: ", o.DataCenters) 78 | } else { 79 | o.Logger.Warning(o.Ctx, "Error initializing oVirt data centers") 80 | } 81 | return nil 82 | } 83 | 84 | // Function that queries ovirt api for clusters and stores them locally. 85 | func (o *OVirtSource) initClusters(conn *ovirtsdk4.Connection) error { 86 | clustersResponse, err := conn.SystemService().ClustersService().List().Send() 87 | if err != nil { 88 | return fmt.Errorf("failed to get oVirt clusters: %v", err) 89 | } 90 | o.Clusters = make(map[string]*ovirtsdk4.Cluster) 91 | if clusters, ok := clustersResponse.Clusters(); ok { 92 | for _, cluster := range clusters.Slice() { 93 | o.Clusters[cluster.MustId()] = cluster 94 | } 95 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt clusters: ", o.Clusters) 96 | } else { 97 | o.Logger.Warning(o.Ctx, "Error initializing oVirt clusters") 98 | } 99 | return nil 100 | } 101 | 102 | // Function that queries ovirt api for hosts and stores them locally. 103 | func (o *OVirtSource) initHosts(conn *ovirtsdk4.Connection) error { 104 | hostsResponse, err := conn.SystemService().HostsService().List().Follow("nics").Send() 105 | if err != nil { 106 | return fmt.Errorf("failed to get oVirt hosts: %+v", err) 107 | } 108 | o.Hosts = make(map[string]*ovirtsdk4.Host) 109 | if hosts, ok := hostsResponse.Hosts(); ok { 110 | for _, host := range hosts.Slice() { 111 | o.Hosts[host.MustId()] = host 112 | } 113 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt hosts: ", hosts) 114 | } else { 115 | o.Logger.Warning(o.Ctx, "Error initializing oVirt hosts") 116 | } 117 | return nil 118 | } 119 | 120 | // Function that queries the ovirt api for vms and stores them locally. 121 | func (o *OVirtSource) initVms(conn *ovirtsdk4.Connection) error { 122 | vmsResponse, err := conn.SystemService(). 123 | VmsService(). 124 | List(). 125 | Follow("nics,diskattachments,reporteddevices"). 126 | Send() 127 | if err != nil { 128 | return fmt.Errorf("failed to get oVirt vms: %+v", err) 129 | } 130 | o.Vms = make(map[string]*ovirtsdk4.Vm) 131 | if vms, ok := vmsResponse.Vms(); ok { 132 | for _, vm := range vms.Slice() { 133 | o.Vms[vm.MustId()] = vm 134 | } 135 | o.Logger.Debug(o.Ctx, "Successfully initialized oVirt vms: ", vms) 136 | } else { 137 | o.Logger.Warning(o.Ctx, "Error initializing oVirt vms") 138 | } 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/source/ovirt/ovirt_test.go: -------------------------------------------------------------------------------- 1 | package ovirt 2 | -------------------------------------------------------------------------------- /internal/source/paloalto/paloalto.go: -------------------------------------------------------------------------------- 1 | package paloalto 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/PaloAltoNetworks/pango" 9 | "github.com/PaloAltoNetworks/pango/netw/interface/eth" 10 | "github.com/PaloAltoNetworks/pango/netw/interface/subinterface/layer3" 11 | "github.com/PaloAltoNetworks/pango/netw/routing/router" 12 | "github.com/PaloAltoNetworks/pango/netw/zone" 13 | "github.com/PaloAltoNetworks/pango/vsys" 14 | "github.com/src-doo/netbox-ssot/internal/constants" 15 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 16 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 17 | "github.com/src-doo/netbox-ssot/internal/source/common" 18 | "github.com/src-doo/netbox-ssot/internal/utils" 19 | ) 20 | 21 | //nolint:revive 22 | type PaloAltoSource struct { 23 | common.Config 24 | // Paloalto data. Initialized in init functions. 25 | SystemInfo map[string]string // Map storing system information 26 | VirtualSystems map[string]vsys.Entry // VirtualSystem name -> VirtualSystem 27 | SecurityZones map[string]zone.Entry // SecurityZone name -> SecurityZone 28 | Iface2SecurityZone map[string]string // Iface name -> SecurityZone name 29 | Iface2VirtualRouter map[string]string // Iface name -> VirtualRouter name 30 | Ifaces map[string]eth.Entry // Iface name -> Iface 31 | Iface2SubIfaces map[string][]layer3.Entry // Iface name -> SubIfaces 32 | VirtualRouters map[string]router.Entry // VirtualRouter name -> VirutalRouter 33 | ArpData []ArpEntry // Array of arp entreies 34 | 35 | // NBFirewall representing paloalto firewall created in syncDevice func. 36 | NBFirewall *objects.Device 37 | } 38 | 39 | func (pas *PaloAltoSource) Init() error { 40 | var transport *http.Transport 41 | var err error 42 | if pas.Config.CAFile != "" { 43 | transport, err = utils.LoadExtraCertInTransportConfig(pas.Config.CAFile) 44 | if err != nil { 45 | return fmt.Errorf("load extra cert in transport config: %s", err) 46 | } 47 | } 48 | c := &pango.Firewall{Client: pango.Client{ 49 | Hostname: pas.SourceConfig.Hostname, 50 | Username: pas.SourceConfig.Username, 51 | Password: pas.SourceConfig.Password, 52 | Logging: pango.LogAction | pango.LogOp, 53 | VerifyCertificate: pas.SourceConfig.ValidateCert, 54 | Port: uint(pas.SourceConfig.Port), //nolint:gosec 55 | Timeout: constants.DefaultAPITimeout, 56 | Protocol: string(pas.SourceConfig.HTTPScheme), 57 | Transport: transport, 58 | }} 59 | 60 | if err := c.Initialize(); err != nil { 61 | return fmt.Errorf("paloalto failed to initialize client: %s", err) 62 | } 63 | 64 | initFunctions := []func(*pango.Firewall) error{ 65 | pas.initArpData, 66 | pas.initSystemInfo, 67 | pas.initVirtualSystems, 68 | pas.initInterfaces, 69 | pas.initVirtualRouters, 70 | } 71 | for _, initFunc := range initFunctions { 72 | startTime := time.Now() 73 | if err := initFunc(c); err != nil { 74 | return fmt.Errorf("paloalto initialization failure: %v", err) 75 | } 76 | duration := time.Since(startTime) 77 | pas.Logger.Infof( 78 | pas.Ctx, 79 | "Successfully initialized %s in %f seconds", 80 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 81 | duration.Seconds(), 82 | ) 83 | } 84 | return nil 85 | } 86 | 87 | func (pas *PaloAltoSource) Sync(nbi *inventory.NetboxInventory) error { 88 | syncFunctions := []func(*inventory.NetboxInventory) error{ 89 | pas.syncDevice, 90 | pas.syncSecurityZones, 91 | pas.syncInterfaces, 92 | pas.syncArpTable, 93 | } 94 | 95 | for _, syncFunc := range syncFunctions { 96 | startTime := time.Now() 97 | err := syncFunc(nbi) 98 | if err != nil { 99 | return err 100 | } 101 | duration := time.Since(startTime) 102 | pas.Logger.Infof( 103 | pas.Ctx, 104 | "Successfully synced %s in %f seconds", 105 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 106 | duration.Seconds(), 107 | ) 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/source/paloalto/paloalto_init.go: -------------------------------------------------------------------------------- 1 | package paloalto 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | 7 | "github.com/PaloAltoNetworks/pango" 8 | "github.com/PaloAltoNetworks/pango/netw/interface/eth" 9 | "github.com/PaloAltoNetworks/pango/netw/interface/subinterface/layer3" 10 | "github.com/PaloAltoNetworks/pango/netw/routing/router" 11 | "github.com/PaloAltoNetworks/pango/netw/zone" 12 | "github.com/PaloAltoNetworks/pango/vsys" 13 | ) 14 | 15 | // Init system info collects system info from paloalto. 16 | func (pas *PaloAltoSource) initSystemInfo(c *pango.Firewall) error { 17 | pas.SystemInfo = c.Client.SystemInfo 18 | return nil 19 | } 20 | 21 | func (pas *PaloAltoSource) initVirtualSystems(c *pango.Firewall) error { 22 | virtualSystems, err := c.Vsys.GetAll() 23 | if err != nil { 24 | return fmt.Errorf("get all virtual systems: %s", err) 25 | } 26 | pas.VirtualSystems = make(map[string]vsys.Entry) 27 | pas.SecurityZones = make(map[string]zone.Entry) 28 | pas.Iface2SecurityZone = make(map[string]string) 29 | for _, virtualSystem := range virtualSystems { 30 | securityZones, err := c.Network.Zone.GetAll(virtualSystem.Name) 31 | if err != nil { 32 | return fmt.Errorf("get zones for virtual system %s: %s", virtualSystem.Name, err) 33 | } 34 | pas.VirtualSystems[virtualSystem.Name] = virtualSystem 35 | for _, securityZone := range securityZones { 36 | pas.SecurityZones[securityZone.Name] = securityZone 37 | for _, iface := range securityZone.Interfaces { 38 | pas.Iface2SecurityZone[iface] = securityZone.Name 39 | } 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func (pas *PaloAltoSource) initVirtualRouters(c *pango.Firewall) error { 46 | routers, err := c.Network.VirtualRouter.GetAll() 47 | if err != nil { 48 | return err 49 | } 50 | pas.VirtualRouters = make(map[string]router.Entry) 51 | pas.Iface2VirtualRouter = make(map[string]string) 52 | for _, router := range routers { 53 | pas.VirtualRouters[router.Name] = router 54 | for _, routerInterface := range router.Interfaces { 55 | pas.Iface2VirtualRouter[routerInterface] = router.Name 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | // initInterfaces collects all ethernet interfaces and subinterfaces 62 | // from paloalto API. It stores them as attribute of the paloalto source. 63 | func (pas *PaloAltoSource) initInterfaces(c *pango.Firewall) error { 64 | ethInterfaces, err := c.Network.EthernetInterface.GetAll() 65 | if err != nil { 66 | return err 67 | } 68 | pas.Ifaces = make(map[string]eth.Entry) 69 | pas.Iface2SubIfaces = make(map[string][]layer3.Entry) 70 | for _, ethInterface := range ethInterfaces { 71 | pas.Ifaces[ethInterface.Name] = ethInterface 72 | subInterfaces, err := c.Network.Layer3Subinterface.GetAll( 73 | layer3.EthernetInterface, 74 | ethInterface.Name, 75 | ) 76 | if err != nil { 77 | return fmt.Errorf("layer 3 subinterfaces: %s", err) 78 | } 79 | pas.Iface2SubIfaces[ethInterface.Name] = make([]layer3.Entry, 0, len(subInterfaces)) 80 | pas.Iface2SubIfaces[ethInterface.Name] = subInterfaces 81 | } 82 | return nil 83 | } 84 | 85 | // Structs to parse xml arp data response. 86 | type ArpData struct { 87 | XMLName xml.Name `xml:"response"` // This ensures the root element is correctly recognized 88 | Status string `xml:"status,attr"` // This captures the "status" attribute in the response tag 89 | Result ArpResult `xml:"result"` // This nests the result struct under the result tag 90 | } 91 | 92 | type ArpResult struct { 93 | Max int `xml:"max"` 94 | Total int `xml:"total"` 95 | Timeout int `xml:"timeout"` 96 | DP string `xml:"dp"` 97 | Entries []ArpEntry `xml:"entries>entry"` // Correct path to entry elements 98 | } 99 | 100 | type ArpEntry struct { 101 | Status string `xml:"status"` 102 | IP string `xml:"ip"` 103 | MAC string `xml:"mac"` 104 | TTL string `xml:"ttl"` 105 | Interface string `xml:"interface"` 106 | Port string `xml:"port"` 107 | } 108 | 109 | // initArpData collects all arp entries from the paloalto source. 110 | // It stores them as attribute of the paloalto source. 111 | func (pas *PaloAltoSource) initArpData(c *pango.Firewall) error { 112 | if pas.SourceConfig.CollectArpData { 113 | var arpData ArpData 114 | arpXMLString := "<show><arp><entry name='all'/></arp></show>" 115 | arpXMLResponse, err := c.Op(arpXMLString, "", nil, nil) 116 | if err != nil { 117 | return fmt.Errorf("init arp data: %s", err) 118 | } 119 | err = xml.Unmarshal(arpXMLResponse, &arpData) 120 | if err != nil { 121 | return fmt.Errorf("init arp data: %s", err) 122 | } 123 | if arpData.Result.Entries != nil { 124 | pas.ArpData = arpData.Result.Entries 125 | } 126 | } 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /internal/source/paloalto/paloalto_test.go: -------------------------------------------------------------------------------- 1 | package paloalto 2 | -------------------------------------------------------------------------------- /internal/source/proxmox/proxmox.go: -------------------------------------------------------------------------------- 1 | package proxmox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/luthermonson/go-proxmox" 9 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 11 | "github.com/src-doo/netbox-ssot/internal/source/common" 12 | "github.com/src-doo/netbox-ssot/internal/utils" 13 | ) 14 | 15 | //nolint:revive 16 | type ProxmoxSource struct { 17 | common.Config 18 | 19 | // Proxmox API data initialized in init functions 20 | Cluster *proxmox.Cluster 21 | Nodes []*proxmox.Node 22 | NodeIfaces map[string][]*proxmox.NodeNetwork // NodeName -> NodeNetworks (interfaces) 23 | Vms map[string][]*proxmox.VirtualMachine // NodeName -> VirtualMachines 24 | VMIfaces map[string][]*proxmox.AgentNetworkIface // VMName -> NetworkDevices 25 | Containers map[string][]*proxmox.Container // NodeName -> Contatiners 26 | ContainerIfaces map[string][]*proxmox.ContainerInterface // ContainerName -> ContainerInterfaces 27 | 28 | // Netbox related data for easier access. Initialized in sync functions. 29 | NetboxCluster *objects.Cluster 30 | NetboxNodes map[string]*objects.Device // NodeName -> netbox device 31 | } 32 | 33 | // Function that collects all data from Proxmox API and stores it in ProxmoxSource struct. 34 | func (ps *ProxmoxSource) Init() error { 35 | // Setup credentials for proxmox 36 | credentials := proxmox.Credentials{ 37 | Username: ps.SourceConfig.Username, 38 | Password: ps.SourceConfig.Password, 39 | } 40 | 41 | // Create http client depending on ssl configuration 42 | HTTPClient, err := utils.NewHTTPClient(ps.SourceConfig.ValidateCert, ps.SourceConfig.CAFile) 43 | if err != nil { 44 | return fmt.Errorf("error creating new HTTP client: %s", err) 45 | } 46 | 47 | // Initialize proxmox client 48 | client := proxmox.NewClient(fmt.Sprintf("%s://%s:%d/api2/json", 49 | ps.SourceConfig.HTTPScheme, ps.SourceConfig.Hostname, ps.SourceConfig.Port), 50 | proxmox.WithCredentials(&credentials), 51 | proxmox.WithHTTPClient(HTTPClient), 52 | ) 53 | 54 | ctx, cancel := context.WithCancel(context.Background()) 55 | defer cancel() 56 | 57 | initFuncs := []func(context.Context, *proxmox.Client) error{ 58 | ps.initCluster, 59 | ps.initNodes, 60 | } 61 | 62 | for _, initFunc := range initFuncs { 63 | startTime := time.Now() 64 | if err := initFunc(ctx, client); err != nil { 65 | return fmt.Errorf("proxmox initialization failure: %v", err) 66 | } 67 | duration := time.Since(startTime) 68 | ps.Logger.Infof( 69 | ps.Ctx, 70 | "Successfully initialized %s in %f seconds", 71 | utils.ExtractFunctionNameWithTrimPrefix(initFunc, "init"), 72 | duration.Seconds(), 73 | ) 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // Function that syncs all collected data to Netbox inventory. 80 | func (ps *ProxmoxSource) Sync(nbi *inventory.NetboxInventory) error { 81 | syncFunctions := []func(*inventory.NetboxInventory) error{ 82 | ps.syncCluster, 83 | ps.syncNodes, 84 | ps.syncVMs, 85 | ps.syncContainers, 86 | } 87 | for _, syncFunc := range syncFunctions { 88 | startTime := time.Now() 89 | err := syncFunc(nbi) 90 | if err != nil { 91 | return err 92 | } 93 | duration := time.Since(startTime) 94 | ps.Logger.Infof( 95 | ps.Ctx, 96 | "Successfully synced %s in %f seconds", 97 | utils.ExtractFunctionNameWithTrimPrefix(syncFunc, "sync"), 98 | duration.Seconds(), 99 | ) 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/source/proxmox/proxmox_init.go: -------------------------------------------------------------------------------- 1 | package proxmox 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/luthermonson/go-proxmox" 8 | ) 9 | 10 | func (ps *ProxmoxSource) initCluster(ctx context.Context, c *proxmox.Client) error { 11 | cluster, err := c.Cluster(ctx) 12 | if err != nil { 13 | return fmt.Errorf("init cluster: %s", err) 14 | } 15 | ps.Cluster = cluster 16 | 17 | return nil 18 | } 19 | 20 | func (ps *ProxmoxSource) initNodes(ctx context.Context, c *proxmox.Client) error { 21 | nodes, err := c.Nodes(ctx) 22 | if err != nil { 23 | return fmt.Errorf("init nodes: %s", err) 24 | } 25 | 26 | ps.Nodes = make([]*proxmox.Node, 0, len(nodes)) 27 | ps.NodeIfaces = make(map[string][]*proxmox.NodeNetwork, len(nodes)) 28 | ps.Vms = make(map[string][]*proxmox.VirtualMachine, len(nodes)) 29 | ps.VMIfaces = make(map[string][]*proxmox.AgentNetworkIface, 0) 30 | ps.Containers = make(map[string][]*proxmox.Container, len(nodes)) 31 | ps.ContainerIfaces = make(map[string][]*proxmox.ContainerInterface, 0) 32 | 33 | for _, node := range nodes { 34 | node, err := c.Node(ctx, node.Node) 35 | if err != nil { 36 | return fmt.Errorf("init node: %s", err) 37 | } 38 | ps.Nodes = append(ps.Nodes, node) 39 | 40 | err = ps.initNodeNetworks(ctx, node) 41 | if err != nil { 42 | return fmt.Errorf("init nodeNetworks: %s", err) 43 | } 44 | 45 | err = ps.initNodeVMs(ctx, node) 46 | if err != nil { 47 | return fmt.Errorf("init nodeVMs: %s", err) 48 | } 49 | 50 | err = ps.initContainers(ctx, node) 51 | if err != nil { 52 | return fmt.Errorf("init node containers: %s", err) 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | // Helper function for initNodes. It collects all nodeNetwork for given node. 59 | func (ps *ProxmoxSource) initNodeNetworks(ctx context.Context, node *proxmox.Node) error { 60 | nodeNetworks, err := node.Networks(ctx) 61 | if err != nil { 62 | return fmt.Errorf("init nodeNetworks: %s", err) 63 | } 64 | ps.NodeIfaces[node.Name] = make([]*proxmox.NodeNetwork, 0, len(nodeNetworks)) 65 | for _, nodeNetwork := range nodeNetworks { 66 | nodeIface, err := node.Network(ctx, nodeNetwork.Iface) 67 | if err != nil { 68 | return fmt.Errorf("init nodeIface: %s", err) 69 | } 70 | ps.NodeIfaces[node.Name] = append(ps.NodeIfaces[node.Name], nodeIface) 71 | } 72 | return nil 73 | } 74 | 75 | // Helper function for initNodes. It collects all vms for given node. 76 | func (ps *ProxmoxSource) initNodeVMs(ctx context.Context, node *proxmox.Node) error { 77 | vms, err := node.VirtualMachines(ctx) 78 | if err != nil { 79 | return err 80 | } 81 | ps.Vms[node.Name] = make([]*proxmox.VirtualMachine, 0, len(vms)) 82 | for _, vm := range vms { 83 | ps.Vms[node.Name] = append(ps.Vms[node.Name], vm) 84 | ifaces, _ := vm.AgentGetNetworkIFaces(ctx) 85 | ps.VMIfaces[vm.Name] = make([]*proxmox.AgentNetworkIface, 0, len(ifaces)) 86 | ps.VMIfaces[vm.Name] = append(ps.VMIfaces[vm.Name], ifaces...) 87 | } 88 | return nil 89 | } 90 | 91 | // Helper function for initNodes. It collects all containers for given node. 92 | func (ps *ProxmoxSource) initContainers(ctx context.Context, node *proxmox.Node) error { 93 | containers, err := node.Containers(ctx) 94 | if err != nil { 95 | return err 96 | } 97 | ps.Containers[node.Name] = make([]*proxmox.Container, 0, len(containers)) 98 | for _, container := range containers { 99 | ps.Containers[node.Name] = append(ps.Containers[node.Name], container) 100 | ifaces, _ := container.Interfaces(ctx) 101 | ps.ContainerIfaces[container.Name] = make([]*proxmox.ContainerInterface, 0, len(ifaces)) 102 | ps.ContainerIfaces[container.Name] = append(ps.ContainerIfaces[container.Name], ifaces...) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /internal/source/proxmox/proxmox_test.go: -------------------------------------------------------------------------------- 1 | package proxmox 2 | -------------------------------------------------------------------------------- /internal/source/source.go: -------------------------------------------------------------------------------- 1 | // Common structs and interfaces for all sources 2 | package source 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | 8 | "github.com/src-doo/netbox-ssot/internal/constants" 9 | "github.com/src-doo/netbox-ssot/internal/logger" 10 | "github.com/src-doo/netbox-ssot/internal/netbox/inventory" 11 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 12 | "github.com/src-doo/netbox-ssot/internal/parser" 13 | "github.com/src-doo/netbox-ssot/internal/source/common" 14 | "github.com/src-doo/netbox-ssot/internal/source/dnac" 15 | "github.com/src-doo/netbox-ssot/internal/source/fmc" 16 | "github.com/src-doo/netbox-ssot/internal/source/fortigate" 17 | iosxe "github.com/src-doo/netbox-ssot/internal/source/ios-xe" 18 | "github.com/src-doo/netbox-ssot/internal/source/ovirt" 19 | "github.com/src-doo/netbox-ssot/internal/source/paloalto" 20 | "github.com/src-doo/netbox-ssot/internal/source/proxmox" 21 | "github.com/src-doo/netbox-ssot/internal/source/vmware" 22 | "github.com/src-doo/netbox-ssot/internal/utils" 23 | ) 24 | 25 | // NewSource creates a Source from the given configuration. 26 | func NewSource( 27 | ctx context.Context, 28 | config *parser.SourceConfig, 29 | logger *logger.Logger, 30 | netboxInventory *inventory.NetboxInventory, 31 | ) (common.Source, error) { 32 | // First we create default tags for the source 33 | sourceNameTag, err := netboxInventory.AddTag(ctx, &objects.Tag{ 34 | Name: config.Tag, 35 | Slug: utils.Slugify("source-" + config.Name), 36 | Color: constants.Color(config.TagColor), 37 | Description: fmt.Sprintf( 38 | "Automatically created tag by netbox-ssot for source %s", 39 | config.Name, 40 | ), 41 | }) 42 | if err != nil { 43 | return nil, fmt.Errorf("error creating sourceTag: %s", err) 44 | } 45 | sourceTypeTag, err := netboxInventory.AddTag(ctx, &objects.Tag{ 46 | Name: string(config.Type), 47 | Slug: utils.Slugify("type-" + string(config.Type)), 48 | Color: constants.Color(constants.SourceTypeTagColorMap[config.Type]), 49 | Description: fmt.Sprintf( 50 | "Automatically created tag by netbox-ssot for source type %s", 51 | config.Type, 52 | ), 53 | }) 54 | if err != nil { 55 | return nil, fmt.Errorf("error creating sourceTypeTag: %s", err) 56 | } 57 | commonConfig := common.Config{ 58 | Logger: logger, 59 | SourceConfig: config, 60 | SourceNameTag: sourceNameTag, 61 | SourceTypeTag: sourceTypeTag, 62 | Ctx: ctx, 63 | CAFile: config.CAFile, 64 | } 65 | 66 | switch config.Type { 67 | case constants.Ovirt: 68 | return &ovirt.OVirtSource{Config: commonConfig}, nil 69 | case constants.Vmware: 70 | return &vmware.VmwareSource{Config: commonConfig}, nil 71 | case constants.Dnac: 72 | return &dnac.DnacSource{Config: commonConfig}, nil 73 | case constants.Proxmox: 74 | return &proxmox.ProxmoxSource{Config: commonConfig}, nil 75 | case constants.PaloAlto: 76 | return &paloalto.PaloAltoSource{Config: commonConfig}, nil 77 | case constants.Fortigate: 78 | return &fortigate.FortigateSource{Config: commonConfig}, nil 79 | case constants.FMC: 80 | return &fmc.FMCSource{Config: commonConfig}, nil 81 | case constants.IOSXE: 82 | return &iosxe.IOSXESource{Config: commonConfig}, nil 83 | default: 84 | return nil, fmt.Errorf("unsupported source type: %s", config.Type) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /internal/source/source_test.go: -------------------------------------------------------------------------------- 1 | package source 2 | -------------------------------------------------------------------------------- /internal/source/vmware/vmware_test.go: -------------------------------------------------------------------------------- 1 | package vmware 2 | -------------------------------------------------------------------------------- /internal/utils/dcim.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/src-doo/netbox-ssot/internal/constants" 9 | ) 10 | 11 | // extractCPUArch extracts the CPU architecture from an input string. 12 | // If no CPU architecture was found, empty string is returned. 13 | func ExtractCPUArch(input string) string { 14 | // Define a regular expression pattern to match common CPU architectures 15 | re := regexp.MustCompile(`(?:x86_64|i[3-6]86|aarch64|arm64|ppc64le|s390x|mips64|riscv64)`) 16 | 17 | // Find the first match 18 | match := re.FindString(input) 19 | 20 | return match 21 | } 22 | 23 | // CPUArchToBits maps cpu architecture to corresponding bits of the architecture: 24 | // x86_64 -> 64-bit, arm -> 32-bit.... 25 | // CPUArchToBits maps cpu architecture to corresponding bits of the architecture. 26 | func CPUArchToBits(arch string) string { 27 | archMap := map[string]string{ 28 | "x86_64": "64-bit", 29 | "amd64": "64-bit", 30 | "i386": "32-bit", 31 | "i486": "32-bit", 32 | "i586": "32-bit", 33 | "i686": "32-bit", 34 | "aarch64": "64-bit", 35 | "arm64": "64-bit", 36 | "arm": "32-bit", 37 | "arm32": "32-bit", 38 | "ppc64le": "64-bit", 39 | "s390x": "64-bit", 40 | "mips64": "64-bit", 41 | "riscv64": "64-bit", 42 | } 43 | 44 | if bits, exists := archMap[arch]; exists { 45 | return bits 46 | } 47 | return arch 48 | } 49 | 50 | // Function that takes osDistribution (Linux, Windows, ...), osMajorVersion (8, 9, 10, ...) 51 | // and cpuArch (x86_64, or 64bit, ....) and generates universal platform name in the format of 52 | // "osDistrbution osMajorVersion (cpuArch)". 53 | func GeneratePlatformName(osDistribution string, osMajorVersion string, cpuArch string) string { 54 | if osDistribution != "" { 55 | osDistribution = SerializeOSName(osDistribution) 56 | } else { 57 | osDistribution = constants.DefaultOSName 58 | } 59 | 60 | if osMajorVersion != "" { 61 | osMajorVersion = fmt.Sprintf(" %s", osMajorVersion) 62 | } 63 | 64 | if cpuArch != "" { 65 | // Check if cpuArch was extreacted from osDistribution 66 | if !strings.Contains(osDistribution, "(") { 67 | cpuArch = fmt.Sprintf(" (%s)", CPUArchToBits(cpuArch)) 68 | } else { 69 | cpuArch = "" 70 | } 71 | } 72 | 73 | return fmt.Sprintf("%s%s%s", osDistribution, osMajorVersion, cpuArch) 74 | } 75 | 76 | // GenerateDeviceTypeSlug generates a device type slug from the given manufacturer and model. 77 | func GenerateDeviceTypeSlug(manufacturerName string, modelName string) string { 78 | manufacturerSlug := Slugify(manufacturerName) 79 | modelSlug := Slugify(modelName) 80 | return fmt.Sprintf("%s-%s", manufacturerSlug, modelSlug) 81 | } 82 | 83 | // ManufacturerMap maps regex of manufacturer names to manufacturer name. 84 | // Manufacturer names are compatible with device type library. See 85 | // internal/devices/combined_data.go for more info. 86 | var ManufacturerMap = map[string]string{ 87 | ".*Cisco.*": "Cisco", 88 | ".*Fortinet.*": "Fortinet", 89 | ".*Dell.*": "Dell", 90 | "FTS Corp": "Fujitsu", 91 | ".*Fujitsu.*": "Fujitsu", 92 | "^HP$": "HPE", 93 | "^HP .*": "HPE", 94 | ".*Huawei.*": "Huawei", 95 | ".*Inspur.*": "Inspur", 96 | ".*Intel.*": "Intel", 97 | "LEN": "Lenovo", 98 | ".*Nvidea.*": "Nvidia", 99 | ".*Samsung.*": "Samsung", 100 | } 101 | 102 | // GetManufactuerFromString returns manufacturer name from the given string. 103 | func SerializeManufacturerName(manufacturer string) string { 104 | for regex, name := range ManufacturerMap { 105 | matched, _ := regexp.MatchString(regex, manufacturer) 106 | if matched { 107 | return name 108 | } 109 | } 110 | return manufacturer 111 | } 112 | 113 | // SpecificOSMap maps regex of OS names to serialized OS names. 114 | var SpecificOSMap = map[string]string{ 115 | "rhcos_x64": "RHCOS (64bit)", 116 | ".*Red Hat Enterprise Linux CoreOS.*": "RHCOS", 117 | 118 | ".*windows_2022.*": "Microsoft Windows 2022", 119 | 120 | ".*ol_5x64.*": "Oracle Linux 5 (64-bit)", 121 | ".*ol_6x64.*": "Oracle Linux 6 (64-bit)", 122 | ".*ol_7x64.*": "Oracle Linux 7 (64-bit)", 123 | ".*ol_8x64.*": "Oracle Linux 8 (64-bit)", 124 | ".*ol_9x64.*": "Oracle Linux 9 (64-bit)", 125 | "Microsoft Windows Server": "Microsoft Windows Server", 126 | } 127 | 128 | // Universal OSMap maps regex of OS names to serialized OS names. 129 | var UniversalOSMap = map[string]string{ 130 | ".*Red Hat Enterprise Linux.*": "RHEL", 131 | 132 | ".*Windows.*": "Microsoft Windows", 133 | 134 | ".*ol_.*": "Oracle Linux", 135 | "^Oracle$": "Oracle Linux", 136 | ".*Oracle Linux Server.*": "Oracle Linux", 137 | 138 | ".*Centos.*": "Centos Linux", 139 | 140 | ".*Rocky.*": "Rocky Linux", 141 | 142 | ".*Alma.*": "Alma Linux", 143 | 144 | ".*Ubuntu.*": "Ubuntu Linux", 145 | } 146 | 147 | // SerializeOSName returns serialized OS name from the given string. 148 | func SerializeOSName(os string) string { 149 | if os == constants.DefaultOSName { 150 | return os 151 | } 152 | for regex, name := range SpecificOSMap { 153 | matched, _ := regexp.MatchString(regex, os) 154 | if matched { 155 | return name 156 | } 157 | } 158 | for regex, name := range UniversalOSMap { 159 | matched, _ := regexp.MatchString(regex, os) 160 | if matched { 161 | return name 162 | } 163 | } 164 | return os 165 | } 166 | -------------------------------------------------------------------------------- /internal/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | // NewHTTPClient creates an http client with tls config depending on validateCert 10 | // and caFile parameter. 11 | func NewHTTPClient(validateCert bool, caFile string) (*http.Client, error) { 12 | httpClient := &http.Client{} 13 | if validateCert { 14 | customCertPool, err := LoadExtraCert(caFile) 15 | if err != nil { 16 | return nil, fmt.Errorf("load extra cert: %s", err) 17 | } 18 | httpClient.Transport = &http.Transport{ 19 | TLSClientConfig: &tls.Config{ 20 | RootCAs: customCertPool, 21 | }, 22 | } 23 | } else { 24 | httpClient.Transport = &http.Transport{ 25 | TLSClientConfig: &tls.Config{ 26 | InsecureSkipVerify: true, 27 | }, 28 | } 29 | } 30 | return httpClient, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/utils/http_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestNewHTTPClient(t *testing.T) { 9 | _, err := NewHTTPClient(false, "") 10 | if err != nil { 11 | t.Errorf("not expecting error, but got: %s", err) 12 | } 13 | 14 | // wrong path 15 | _, err = NewHTTPClient(true, "\\//") 16 | if err == nil { 17 | t.Error("expected error but got none") 18 | } 19 | 20 | // Check if `InsecureSkipVerify` is set correctly 21 | insecureClient, err := NewHTTPClient(false, "") 22 | if err != nil { 23 | t.Fatalf("Expected no error, got %v", err) 24 | } 25 | transport := insecureClient.Transport.(*http.Transport) //nolint:forcetypeassert 26 | if transport.TLSClientConfig.InsecureSkipVerify != true { 27 | t.Errorf("expected InsecureSkipVerify to be true, got false") 28 | } 29 | 30 | // Check if RootCAs is set when expected 31 | certClient, err := NewHTTPClient(true, "") 32 | if err != nil { 33 | t.Fatalf("Expected no error, got %v", err) 34 | } 35 | transport = certClient.Transport.(*http.Transport) //nolint:forcetypeassert 36 | if transport.TLSClientConfig.RootCAs == nil { 37 | t.Errorf("expected RootCAs to be set, got nil") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/utils/json.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | func ExtractJSONTagsFromStructIntoString(inputStruct interface{}) string { 9 | jsonFields := ExtractJSONTagsFromStruct(inputStruct) 10 | return strings.Join(jsonFields, ",") 11 | } 12 | 13 | func ExtractJSONTagsFromStruct(inputStruct interface{}) []string { 14 | var jsonFields []string 15 | 16 | // Helper function to recursively extract JSON tags 17 | var extractFields func(reflect.Type) 18 | extractFields = func(t reflect.Type) { 19 | // If the type is a pointer, dereference it 20 | if t.Kind() == reflect.Ptr { 21 | t = t.Elem() 22 | } 23 | // Ensure the type is a struct 24 | if t.Kind() != reflect.Struct { 25 | return 26 | } 27 | 28 | // Iterate through struct fields 29 | for i := 0; i < t.NumField(); i++ { 30 | field := t.Field(i) 31 | 32 | // Check if the field is embedded 33 | if field.Anonymous { 34 | // Recursively process the embedded struct 35 | extractFields(field.Type) 36 | continue 37 | } 38 | 39 | // Get the JSON tag 40 | tag := field.Tag.Get("json") 41 | if tag != "" && tag != "-" { 42 | // Handle "omitempty" or other tags (split by comma) 43 | tagParts := strings.Split(tag, ",") 44 | jsonFields = append(jsonFields, tagParts[0]) 45 | } 46 | } 47 | } 48 | 49 | // Start extracting fields from the input struct 50 | t := reflect.TypeOf(inputStruct) 51 | extractFields(t) 52 | 53 | return jsonFields 54 | } 55 | -------------------------------------------------------------------------------- /internal/utils/json_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/src-doo/netbox-ssot/internal/netbox/objects" 8 | ) 9 | 10 | func TestExtractJSONTagsFromStruct(t *testing.T) { 11 | type args struct { 12 | inputStruct interface{} 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want []string 18 | }{ 19 | { 20 | name: "Extract fields from tag", 21 | args: args{ 22 | inputStruct: objects.Tag{}, 23 | }, 24 | want: []string{"id", "name", "slug", "color", "description"}, 25 | }, 26 | { 27 | name: "Extract fields from custom field", 28 | args: args{ 29 | inputStruct: objects.CustomField{}, 30 | }, 31 | want: []string{ 32 | "id", 33 | "name", 34 | "label", 35 | "type", 36 | "object_types", 37 | "description", 38 | "search_weight", 39 | "filter_logic", 40 | "ui_visible", 41 | "ui_editable", 42 | "weight", 43 | "default", 44 | "required", 45 | }, 46 | }, 47 | { 48 | name: "Extract json fields from device", 49 | args: args{ 50 | inputStruct: objects.Device{}, 51 | }, 52 | want: []string{ 53 | "id", 54 | "tags", 55 | "description", 56 | "custom_fields", 57 | "name", 58 | "role", 59 | "device_type", 60 | "airflow", 61 | "serial", 62 | "asset_tag", 63 | "site", 64 | "location", 65 | "status", 66 | "platform", 67 | "primary_ip4", 68 | "primary_ip6", 69 | "cluster", 70 | "tenant", 71 | "comments", 72 | }, 73 | }, 74 | { 75 | name: "Extract fields from VMInterface", 76 | args: args{ 77 | inputStruct: objects.VMInterface{}, 78 | }, 79 | want: []string{ 80 | "id", 81 | "tags", 82 | "description", 83 | "custom_fields", 84 | "virtual_machine", 85 | "name", 86 | "primary_mac_address", 87 | "mtu", 88 | "enabled", 89 | "parent", 90 | "bridge", 91 | "mode", 92 | "tagged_vlans", 93 | "untagged_vlan", 94 | }, 95 | }, 96 | { 97 | name: "Extract fields from interface", 98 | args: args{ 99 | inputStruct: objects.Interface{}, 100 | }, 101 | want: []string{ 102 | "id", 103 | "tags", 104 | "description", 105 | "custom_fields", 106 | "device", 107 | "name", 108 | "enabled", 109 | "type", 110 | "speed", 111 | "parent", 112 | "bridge", 113 | "lag", 114 | "mtu", 115 | "primary_mac_address", 116 | "duplex", 117 | "mode", 118 | "tagged_vlans", 119 | "untagged_vlan", 120 | "vdcs", 121 | }, 122 | }, 123 | } 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | if got := ExtractJSONTagsFromStruct(tt.args.inputStruct); !reflect.DeepEqual( 127 | got, 128 | tt.want, 129 | ) { 130 | t.Errorf("ExtractStructJSONFields() = %v, want %v", got, tt.want) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestExtractJSONTagsFromStructIntoString(t *testing.T) { 137 | type args struct { 138 | inputStruct interface{} 139 | } 140 | tests := []struct { 141 | name string 142 | args args 143 | want string 144 | }{ 145 | { 146 | name: "Extract fields from tag", 147 | args: args{ 148 | inputStruct: objects.Tag{}, 149 | }, 150 | want: "id,name,slug,color,description", 151 | }, 152 | } 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | if got := ExtractJSONTagsFromStructIntoString(tt.args.inputStruct); got != tt.want { 156 | t.Errorf("ExtractJSONTagsFromStructIntoString() = %v, want %v", got, tt.want) 157 | } 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /internal/utils/netbox_marshal.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // NetboxJSONMarshal takes an object pointer, and returns a json body, 10 | // that can be used to create that object in netbox API. 11 | // This is essential because default marshal of the object 12 | // isn't compatible with netbox API when attributes have nested 13 | // objects. 14 | func NetboxJSONMarshal(obj interface{}) ([]byte, error) { 15 | objMap := StructToNetboxJSONMap(obj) 16 | json, err := json.Marshal(objMap) 17 | return json, err 18 | } 19 | 20 | // StructToNetboxJSONMap converts an object to a map[string]interface{} 21 | // which can be used to create a json body for netbox API, especially 22 | // for POST requests. 23 | func StructToNetboxJSONMap(obj interface{}) map[string]interface{} { 24 | v := reflect.ValueOf(obj) 25 | if v.Kind() == reflect.Ptr { 26 | v = v.Elem() 27 | } 28 | 29 | netboxJSONMap := make(map[string]interface{}) 30 | for i := 0; i < v.NumField(); i++ { 31 | fieldValue := v.Field(i) 32 | fieldType := v.Type().Field(i) 33 | jsonTag := fieldType.Tag.Get("json") 34 | jsonTag = strings.Split(jsonTag, ",")[0] 35 | 36 | if fieldType.Name == "ID" { 37 | continue 38 | } 39 | 40 | // Special case when object inherits from NetboxObject 41 | if fieldType.Name == "NetboxObject" { 42 | diffMap := StructToNetboxJSONMap(fieldValue.Interface()) 43 | for k, v := range diffMap { 44 | netboxJSONMap[k] = v 45 | } 46 | continue 47 | } 48 | 49 | // If field is a pointer, we need to get the element it points to 50 | if fieldValue.Kind() == reflect.Ptr { 51 | // Filter out nil pointers 52 | if fieldValue.IsNil() { 53 | continue 54 | } 55 | fieldValue = fieldValue.Elem() 56 | } 57 | 58 | // If a field is empty we skip it 59 | if !fieldValue.IsValid() || fieldValue.IsZero() { 60 | continue 61 | } 62 | 63 | switch fieldValue.Kind() { 64 | case reflect.Slice: 65 | if fieldValue.Len() == 0 { 66 | continue 67 | } 68 | sliceItems := make([]interface{}, 0) 69 | for j := 0; j < fieldValue.Len(); j++ { 70 | attribute := fieldValue.Index(j) 71 | if attribute.Kind() == reflect.Ptr { 72 | // Filter out nil pointers 73 | if attribute.IsNil() { 74 | continue 75 | } 76 | attribute = attribute.Elem() 77 | } 78 | if attribute.Kind() == reflect.Struct { 79 | id := attribute.FieldByName("ID") 80 | if id.IsValid() && id.Int() != 0 { 81 | sliceItems = append(sliceItems, id.Int()) 82 | } else { 83 | sliceItems = append(sliceItems, attribute.Interface()) 84 | } 85 | } else { 86 | sliceItems = append(sliceItems, attribute.Interface()) 87 | } 88 | } 89 | // Slices with only nil values are skipped 90 | if len(sliceItems) == 0 { 91 | continue 92 | } 93 | netboxJSONMap[jsonTag] = sliceItems 94 | case reflect.Struct: 95 | if isChoiceEmbedded(fieldValue) { 96 | choiceValue := fieldValue.FieldByName("Value") 97 | if choiceValue.IsValid() { 98 | netboxJSONMap[jsonTag] = choiceValue.Interface() 99 | } 100 | } else { 101 | id := fieldValue.FieldByName("ID") 102 | if id.IsValid() { 103 | netboxJSONMap[jsonTag] = id.Int() 104 | } else { 105 | netboxJSONMap[jsonTag] = fieldValue.Interface() 106 | } 107 | } 108 | default: 109 | netboxJSONMap[jsonTag] = fieldValue.Interface() 110 | } 111 | } 112 | return netboxJSONMap 113 | } 114 | -------------------------------------------------------------------------------- /internal/utils/networking.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/src-doo/netbox-ssot/internal/constants" 11 | ) 12 | 13 | func ReverseLookup(ipAddress string) string { 14 | // Create a context with the specified timeout 15 | TIMEOUT := 2 * time.Second //nolint:mnd 16 | ctx, cancel := context.WithTimeout(context.Background(), TIMEOUT) 17 | defer cancel() 18 | 19 | // Check if ipAddress contains a mask, and remove it 20 | ipAddress = strings.Split(ipAddress, "/")[0] 21 | 22 | // Use a custom resolver with the context 23 | resolver := &net.Resolver{} 24 | names, err := resolver.LookupAddr(ctx, ipAddress) 25 | if err != nil || len(names) == 0 { 26 | return "" 27 | } 28 | 29 | // Return the first domain name, stripping the trailing dot if present 30 | domain := strings.TrimSuffix(names[0], ".") 31 | return domain 32 | } 33 | 34 | // Function that receives hostname and performs a forward lookup 35 | // to get the IP address. If the forward lookup fails, it returns an empty string. 36 | func Lookup(hostname string) string { 37 | ips, err := net.LookupIP(hostname) 38 | if err != nil || len(ips) == 0 { 39 | return "" 40 | } 41 | return ips[0].String() 42 | } 43 | 44 | // SerializeMask serializes mask into a bit representation. 45 | // If a mask is already in bit rerpesentation, it returns the mask: 46 | // mask "24" -> "24", 47 | // mask "255.255.255.0" -> "24". 48 | func SerializeMask(mask string) string { 49 | if strings.Contains(mask, ".") { 50 | maskBits, _ := MaskToBits(mask) 51 | return fmt.Sprintf("%d", maskBits) 52 | } 53 | return mask 54 | } 55 | 56 | // Function that converts string representation of ipv4 mask (e.g. 255.255.255.128) to 57 | // bit representation (e.g. 25). 58 | func MaskToBits(mask string) (int, error) { 59 | ipMask := net.IPMask(net.ParseIP(mask).To4()) 60 | if ipMask == nil { 61 | return 0, fmt.Errorf("invalid mask: %s", mask) 62 | } 63 | ones, _ := ipMask.Size() 64 | return ones, nil 65 | } 66 | 67 | // GetIPVersion returns the version of the IP address. 68 | // It returns 4 for IPv4, 6 for IPv6, and 0 if the IP address is invalid. 69 | func GetIPVersion(ipAddress string) int { 70 | ip := net.ParseIP(ipAddress) 71 | if ip == nil { 72 | return 0 73 | } 74 | if ip.To4() != nil { 75 | return constants.IPv4 76 | } 77 | return constants.IPv6 78 | } 79 | 80 | // RemoveZoneIndexFromIPAddress removes zone index from the IPv6 address: 81 | // e.g. 2001:db8::1%eth0 -> 2001:db8::1. 82 | // e.g. 2001:db8::1%2/64 -> 2001:db8::1/64. 83 | func RemoveZoneIndexFromIPAddress(ipAddress string) string { 84 | if strings.Contains(ipAddress, "%") { 85 | base := strings.Split(ipAddress, "%")[0] 86 | maskArr := strings.Split(ipAddress, "/") 87 | mask := "" 88 | if len(maskArr) > 1 { 89 | mask = "/" + maskArr[1] 90 | } 91 | ipAddress = base + mask 92 | } 93 | return ipAddress 94 | } 95 | 96 | // SubnetContainsIPAddress checks if given IP address is part of the 97 | // given subnet (e.g. ipAddress "172.31.4.129" and 98 | // subnet "172.31.4.145/25"). 99 | func SubnetContainsIPAddress(ipAddress string, subnet string) bool { 100 | ipAddress = RemoveZoneIndexFromIPAddress(ipAddress) 101 | address := strings.Split(ipAddress, "/")[0] 102 | ip := net.ParseIP(address) 103 | if ip == nil { 104 | return false 105 | } 106 | _, ipnet, err := net.ParseCIDR(subnet) 107 | if err != nil { 108 | return false 109 | } 110 | return ipnet.Contains(ip) 111 | } 112 | 113 | // VerifySubnet checks if a given subnet is valid. 114 | func VerifySubnet(subnet string) bool { 115 | _, _, err := net.ParseCIDR(subnet) 116 | return err == nil 117 | } 118 | 119 | // subnetsContainIPAddress checks if array of subnets contain, 120 | // the ip address. 121 | func subnetsContainIPAddress(ipAddress string, subnets []string) bool { 122 | for _, subnet := range subnets { 123 | if SubnetContainsIPAddress(ipAddress, subnet) { 124 | return true 125 | } 126 | } 127 | return false 128 | } 129 | 130 | func IsPermittedIPAddress( 131 | ipAddress string, 132 | permittedSubnets []string, 133 | ignoredSubnets []string, 134 | ) bool { 135 | if subnetsContainIPAddress(ipAddress, ignoredSubnets) { 136 | return false 137 | } 138 | if len(permittedSubnets) == 0 { 139 | return true 140 | } 141 | return subnetsContainIPAddress(ipAddress, permittedSubnets) 142 | } 143 | 144 | // GetmaskAndPrefixFromIPAddress extracts mask and prefix 145 | // from a given ipAddress of format ip/mask. 146 | // 192.168.1.1/24 --> (192.168.1.0/24, 24). 147 | func GetPrefixAndMaskFromIPAddress(ipAddress string) (string, int, error) { 148 | _, ipNet, err := net.ParseCIDR(ipAddress) 149 | if err != nil { 150 | return "", 0, err 151 | } 152 | maskBits, _ := ipNet.Mask.Size() 153 | return ipNet.String(), maskBits, err 154 | } 155 | -------------------------------------------------------------------------------- /k8s/cronjob.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: netbox-ssot 5 | spec: 6 | schedule: "*/20 * * * *" 7 | jobTemplate: 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: netbox-ssot 13 | image: ghcr.io/src-doo/netbox-ssot:v1.11.6 14 | imagePullPolicy: Always 15 | resources: 16 | limits: 17 | cpu: 200m 18 | memory: 256Mi 19 | requests: 20 | cpu: 100m 21 | memory: 128Mi 22 | volumeMounts: 23 | - name: netbox-ssot-secret 24 | mountPath: /app/config.yaml 25 | subPath: config.yaml 26 | securityContext: 27 | allowPrivilegeEscalation: false 28 | capabilities: 29 | drop: ["ALL"] 30 | runAsNonRoot: true 31 | readOnlyRootFilesystem: true 32 | runAsUser: 10001 33 | runAsGroup: 10001 34 | seccompProfile: 35 | type: RuntimeDefault 36 | volumes: 37 | - name: netbox-ssot-secret 38 | secret: 39 | secretName: netbox-ssot-secret 40 | restartPolicy: Never 41 | -------------------------------------------------------------------------------- /k8s/cronjob_with_cert.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: netbox-ssot 5 | spec: 6 | schedule: "*/20 * * * *" 7 | jobTemplate: 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: netbox-ssot 13 | image: ghcr.io/src-doo/netbox-ssot:v1.11.6 14 | imagePullPolicy: Always 15 | resources: 16 | limits: 17 | cpu: 200m 18 | memory: 256Mi 19 | requests: 20 | cpu: 100m 21 | memory: 128Mi 22 | volumeMounts: 23 | - name: netbox-ssot-secret 24 | mountPath: /app/config.yaml 25 | subPath: config.yaml 26 | - name: netbox-ssot-cert 27 | mountPath: /app/sub.pem 28 | subPath: sub.pem 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: ["ALL"] 33 | runAsNonRoot: true 34 | readOnlyRootFilesystem: true 35 | runAsUser: 10001 36 | runAsGroup: 10001 37 | seccompProfile: 38 | type: RuntimeDefault 39 | volumes: 40 | - name: netbox-ssot-secret 41 | secret: 42 | secretName: netbox-ssot-secret 43 | - name: netbox-ssot-cert 44 | secret: 45 | secretName: netbox-ssot-cert 46 | restartPolicy: OnFailure 47 | -------------------------------------------------------------------------------- /project-words.txt: -------------------------------------------------------------------------------- 1 | adminpass 2 | automerge 3 | BASEFX 4 | BASELFX 5 | BASET 6 | BASETX 7 | Bips 8 | calldepth 9 | cddc 10 | CDFP 11 | CDMA 12 | CPAK 13 | datacenter 14 | Datacenters 15 | dcim 16 | Debugf 17 | diskattachments 18 | Dnac 19 | dnacenter 20 | DSFP 21 | dvpg 22 | dvpgs 23 | Dvswitch 24 | epon 25 | errcheck 26 | ffeb 27 | gbase 28 | GBIC 29 | GBPS 30 | GEPON 31 | gitleaks 32 | glbp 33 | GOARCH 34 | goinstall 35 | golangci 36 | gomod 37 | govmomi 38 | gpon 39 | HOSTSTATUS 40 | hsrp 41 | Hynix 42 | iface 43 | Infof 44 | Inspur 45 | intf 46 | ipam 47 | ipnet 48 | ipvs 49 | Kbps 50 | longtext 51 | lycheeverse 52 | MBPS 53 | netbox 54 | NGPON 55 | nics 56 | NICSTATUS 57 | nonspacing 58 | Nvidea 59 | olvm 60 | OSFP 61 | OSFPRHS 62 | ovirt 63 | ovirtsdk 64 | Pgroup 65 | pnic 66 | pnics 67 | Portgroup 68 | Portgroups 69 | prodolvm 70 | prodovirt 71 | prodvcenter 72 | pswitch 73 | Pvlan 74 | QSFP 75 | QSFPDD 76 | qsfpp 77 | reporteddevices 78 | rojopolis 79 | SFPDD 80 | SFPP 81 | slaac 82 | SLAAC 83 | slugified 84 | Sriov 85 | ssot 86 | Supermicro 87 | testdata 88 | testolvm 89 | teststring 90 | testvcenter 91 | topsecret 92 | TWDM 93 | VCPUs 94 | veth 95 | virbr 96 | Virt 97 | virtualmachine 98 | vlangroups 99 | VLANID 100 | vminterface 101 | VMSTATUS 102 | vnic 103 | VRRP 104 | vsphere 105 | vxlan 106 | Warningf 107 | wordlists 108 | XENPAK 109 | XGPON 110 | XGSPON 111 | buildx 112 | prodvmware 113 | dockerhub 114 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "baseBranches": ["main"], 4 | "extends": [ 5 | "config:recommended", 6 | "docker:pinDigests", 7 | "helpers:pinGitHubActionDigests", 8 | ":pinDevDependencies" 9 | ], 10 | "pre-commit": { 11 | "enabled": true 12 | }, 13 | "regexManagers": [ 14 | { 15 | "description": "Update semantic-release version used by npx", 16 | "fileMatch": ["^\\.github/workflows/[^/]+\\.ya?ml$"], 17 | "matchStrings": ["\\srun: npx semantic-release@(?<currentValue>.*?)\\s"], 18 | "datasourceTemplate": "npm", 19 | "depNameTemplate": "semantic-release" 20 | } 21 | ], 22 | "packageRules": [ 23 | { 24 | "matchManagers": ["gomod"], 25 | "groupName": "go dependencies", 26 | "automerge": true 27 | }, 28 | { 29 | "matchManagers": ["github-actions"], 30 | "groupName": "github actions", 31 | "automerge": true 32 | }, 33 | { 34 | "matchManagers": ["dockerfile"], 35 | "groupName": "dockerfile dependencies", 36 | "automerge": true 37 | }, 38 | { 39 | "matchManagers": ["regex"], 40 | "groupName": "semantic-release", 41 | "automerge": true, 42 | "automergeStrategy": "rebase" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /testdata/certificate/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIHbjCCBlagAwIBAgIQB1vO8waJyK3fE+Ua9K/hhzANBgkqhkiG9w0BAQsFADBZ 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMTMwMQYDVQQDEypE 4 | aWdpQ2VydCBHbG9iYWwgRzIgVExTIFJTQSBTSEEyNTYgMjAyMCBDQTEwHhcNMjQw 5 | MTMwMDAwMDAwWhcNMjUwMzAxMjM1OTU5WjCBljELMAkGA1UEBhMCVVMxEzARBgNV 6 | BAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMUIwQAYDVQQKDDlJ 7 | bnRlcm5ldMKgQ29ycG9yYXRpb27CoGZvcsKgQXNzaWduZWTCoE5hbWVzwqBhbmTC 8 | oE51bWJlcnMxGDAWBgNVBAMTD3d3dy5leGFtcGxlLm9yZzCCASIwDQYJKoZIhvcN 9 | AQEBBQADggEPADCCAQoCggEBAIaFD7sO+cpf2fXgCjIsM9mqDgcpqC8IrXi9wga/ 10 | 9y0rpqcnPVOmTMNLsid3INbBVEm4CNr5cKlh9rJJnWlX2vttJDRyLkfwBD+dsVvi 11 | vGYxWTLmqX6/1LDUZPVrynv/cltemtg/1Aay88jcj2ZaRoRmqBgVeacIzgU8+zmJ 12 | 7236TnFSe7fkoKSclsBhPaQKcE3Djs1uszJs8sdECQTdoFX9I6UgeLKFXtg7rRf/ 13 | hcW5dI0zubhXbrW8aWXbCzySVZn0c7RkJMpnTCiZzNxnPXnHFpwr5quqqjVyN/aB 14 | KkjoP04Zmr+eRqoyk/+lslq0sS8eaYSSHbC5ja/yMWyVhvMCAwEAAaOCA/IwggPu 15 | MB8GA1UdIwQYMBaAFHSFgMBmx9833s+9KTeqAx2+7c0XMB0GA1UdDgQWBBRM/tAS 16 | TS4hz2v68vK4TEkCHTGRijCBgQYDVR0RBHoweIIPd3d3LmV4YW1wbGUub3Jnggtl 17 | eGFtcGxlLm5ldIILZXhhbXBsZS5lZHWCC2V4YW1wbGUuY29tggtleGFtcGxlLm9y 18 | Z4IPd3d3LmV4YW1wbGUuY29tgg93d3cuZXhhbXBsZS5lZHWCD3d3dy5leGFtcGxl 19 | Lm5ldDA+BgNVHSAENzA1MDMGBmeBDAECAjApMCcGCCsGAQUFBwIBFhtodHRwOi8v 20 | d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG 21 | CCsGAQUFBwMBBggrBgEFBQcDAjCBnwYDVR0fBIGXMIGUMEigRqBEhkJodHRwOi8v 22 | Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxHMlRMU1JTQVNIQTI1NjIw 23 | MjBDQTEtMS5jcmwwSKBGoESGQmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdp 24 | Q2VydEdsb2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNybDCBhwYIKwYBBQUH 25 | AQEEezB5MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wUQYI 26 | KwYBBQUHMAKGRWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEds 27 | b2JhbEcyVExTUlNBU0hBMjU2MjAyMENBMS0xLmNydDAMBgNVHRMBAf8EAjAAMIIB 28 | fQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdABOdaMnXJoQwzhbbNTfP1LrHfDgjhuN 29 | acCx+mSxYpo53wAAAY1b0vxkAAAEAwBFMEMCH0BRCgxPbBBVxhcWZ26a8JCe83P1 30 | JZ6wmv56GsVcyMACIDgpMbEo5HJITTRPnoyT4mG8cLrWjEvhchUdEcWUuk1TAHYA 31 | fVkeEuF4KnscYWd8Xv340IdcFKBOlZ65Ay/ZDowuebgAAAGNW9L8MAAABAMARzBF 32 | AiBdv5Z3pZFbfgoM3tGpCTM3ZxBMQsxBRSdTS6d8d2NAcwIhALLoCT9mTMN9OyFz 33 | IBV5MkXVLyuTf2OAzAOa7d8x2H6XAHcA5tIxY0B3jMEQQQbXcbnOwdJA9paEhvu6 34 | hzId/R43jlAAAAGNW9L8XwAABAMASDBGAiEA4Koh/VizdQU1tjZ2E2VGgWSXXkwn 35 | QmiYhmAeKcVLHeACIQD7JIGFsdGol7kss2pe4lYrCgPVc+iGZkuqnj26hqhr0TAN 36 | BgkqhkiG9w0BAQsFAAOCAQEABOFuAj4N4yNG9OOWNQWTNSICC4Rd4nOG1HRP/Bsn 37 | rz7KrcPORtb6D+Jx+Q0amhO31QhIvVBYs14gY4Ypyj7MzHgm4VmPXcqLvEkxb2G9 38 | Qv9hYuEiNSQmm1fr5QAN/0AzbEbCM3cImLJ69kP5bUjfv/76KB57is8tYf9sh5ik 39 | LGKauxCM/zRIcGa3bXLDafk5S2g5Vr2hs230d/NGW1wZrE+zdGuMxfGJzJP+DAFv 40 | iBfcQnFg4+1zMEKcqS87oniOyG+60RMM0MdejBD7AS43m9us96Gsun/4kufLQUTI 41 | FfnzxLutUV++3seshgefQOy5C/ayi8y1VTNmujPCxPCi6Q== 42 | -----END CERTIFICATE----- 43 | -------------------------------------------------------------------------------- /testdata/certificate/invalid_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN INVALID CERTIFICATE----- 2 | I'm invalid certificate. 3 | -----END INVALID CERTIFCIATE----- 4 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config1.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | 9 | source: 10 | - name: testolvm 11 | type: ovirt 12 | hostname: testolvm.example.com 13 | username: admin@internal 14 | password: adminpass 15 | permittedSubnets: 16 | - 172.16.0.0/12 17 | - 192.168.0.0/16 18 | - fd00::/8 19 | validateCert: true 20 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config10.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | dest: "test" 3 | 4 | netbox: 5 | apiToken: "dummytoken" 6 | port: 666 7 | hostname: netbox.example.com 8 | timeout: -1 9 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config11.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | dest: "test" 3 | 4 | netbox: 5 | apiToken: "" # cannot be empty 6 | port: 666 7 | hostname: netbox.example.com 8 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config12.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | dest: "test" 3 | 4 | netbox: 5 | apiToken: dummy 6 | port: 666 7 | hostname: netbox.example.com 8 | tagColor: fffffff 9 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config13.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | dest: "test" 3 | 4 | netbox: 5 | apiToken: dummy 6 | port: 666 7 | hostname: netbox.example.com 8 | tagColor: ffFFff 9 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config14.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | sourcePriority: ["testolvm", "testvcenter"] # sourcePriority doesn't contain all sources 11 | 12 | source: 13 | - name: testolvm 14 | type: ovirt 15 | hostname: testolvm.example.com 16 | username: admin@internal 17 | password: adminpass 18 | permittedSubnets: 19 | - 172.16.0.0/12 20 | - 192.168.0.0/16 21 | - fd00::/8 22 | validateCert: true 23 | 24 | - name: testvcenter 25 | type: vmware 26 | hostname: testvcenter.example.com 27 | username: admin 28 | password: adminpass 29 | permittedSubnets: 30 | - 172.16.0.0/12 31 | 32 | - name: prodvcenter 33 | type: vmware 34 | hostname: prodvcenter.example.com 35 | username: test 36 | password: test 37 | permittedSubnets: 38 | - 10.0.0.0/8 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config15.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | sourcePriority: ["testolvm", "testvcenter", "wrongone"] # sourcePriority doesn't contain all sources 11 | 12 | source: 13 | - name: testolvm 14 | type: ovirt 15 | hostname: testolvm.example.com 16 | username: admin@internal 17 | password: adminpass 18 | permittedSubnets: 19 | - 172.16.0.0/12 20 | - 192.168.0.0/16 21 | - fd00::/8 22 | validateCert: true 23 | 24 | - name: testvcenter 25 | type: vmware 26 | hostname: testvcenter.example.com 27 | username: admin 28 | password: adminpass 29 | permittedSubnets: 30 | - 172.16.0.0/12 31 | 32 | - name: prodvcenter 33 | type: vmware 34 | hostname: prodvcenter.example.com 35 | username: test 36 | password: test 37 | permittedSubnets: 38 | - 10.0.0.0/8 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config16.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | sourcePriority: ["testolvm", "testvcenter", ""] 11 | 12 | source: 13 | - name: testolvm 14 | type: ovirt 15 | hostname: testolvm.example.com 16 | username: admin@internal 17 | password: adminpass 18 | permittedSubnets: 19 | - 172.16.0.0/12 20 | - 192.168.0.0/16 21 | - fd00::/8 22 | validateCert: true 23 | 24 | - name: testvcenter 25 | type: vmware 26 | hostname: testvcenter.example.com 27 | username: admin 28 | password: adminpass 29 | permittedSubnets: 30 | - 172.16.0.0/12 31 | 32 | - name: "" # wrong: empty name 33 | type: vmware 34 | hostname: prodvcenter.example.com 35 | username: test 36 | password: test 37 | permittedSubnets: 38 | - 10.0.0.0/8 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config17.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | sourcePriority: ["testolvm", "testvcenter", "wrong"] 11 | 12 | source: 13 | - name: testolvm 14 | type: ovirt 15 | hostname: testolvm.example.com 16 | username: admin@internal 17 | password: adminpass 18 | permittedSubnets: 19 | - 172.16.0.0/12 20 | - 192.168.0.0/16 21 | - fd00::/8 22 | validateCert: true 23 | 24 | - name: testvcenter 25 | type: vmware 26 | hostname: testvcenter.example.com 27 | username: admin 28 | password: adminpass 29 | permittedSubnets: 30 | - 172.16.0.0/12 31 | 32 | - name: wrong 33 | type: vmware 34 | hostname: "" # cannot be empty 35 | username: test 36 | password: test 37 | permittedSubnets: 38 | - 10.0.0.0/8 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config18.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "" # cannot be empty 16 | password: adminpass 17 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config19.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "" # cannot be empty 17 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config2.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | hostname: netbox.example.com 8 | port: 333333 9 | 10 | source: 11 | - name: testolvm 12 | type: ovirt 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | permittedSubnets: 17 | - 172.16.0.0/12 18 | - 192.168.0.0/16 19 | - fd00::/8 20 | validateCert: true 21 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config20.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | ignoredSubnets: 18 | - 192.168.1.24/20 19 | - 172.16.0.1/16 20 | - 172.16.0.1 # Wrong format 21 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config21.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | interfaceFilter: ($a[ba] # wrong regex format 18 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config22.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | hostSiteRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config23.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | clusterSiteRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config24.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | clusterTenantRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config25.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | hostTenantRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config26.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vmTenantRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config27.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanGroupRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config28.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanTenantRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config29.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "2dasf" # decoder error 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config3.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | hostname: netbox.example.com 8 | port: 3333 9 | 10 | source: 11 | - name: testolvm 12 | type: unknown 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | permittedSubnets: 17 | - 172.16.0.0/12 18 | - 192.168.0.0/16 19 | - fd00::/8 20 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config30.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: paloalto 13 | type: paloalto 14 | hostname: paloalto.example.com 15 | username: user 16 | password: pass 17 | 18 | - name: fortigate 19 | type: fortigate 20 | hostname: forti.example.com 21 | apiToken: "" # Error apitoken must be provided 22 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config31.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | caFile: "wrong path" # error 11 | removeOrphans: True 12 | removeOrphansAfterDays: 5 # error because removeOrphans is set to True 13 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config32.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: fmc 14 | hostname: fmc.example.com 15 | username: "test" 16 | password: "test" 17 | datacenterClusterGroupRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config33.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: fmc 14 | hostname: fmc.example.com 15 | username: "test" 16 | password: "test" 17 | caFile: "\\//" # wrong path 18 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config34.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | caFile: "wrong path" # error 11 | 12 | source: 13 | - name: wrong 14 | type: fmc 15 | hostname: fmc.example.com 16 | username: "test" 17 | password: "test" 18 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config35.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | removeOrphans: False 11 | removeOrphansAfterDays: -1 # Error because removeOrphansAfterDays must be positive number 12 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config36.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: fmc 14 | hostname: fmc.example.com 15 | username: "test" 16 | password: "test" 17 | wlanTenantRelations: # Wrong wlanTenantRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config37.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: 7 # Wrong dest should be string 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config38.yaml: -------------------------------------------------------------------------------- 1 | logger: "this should be a map" 2 | 3 | netbox: 4 | apiToken: "netbox-token" 5 | port: 666 6 | hostname: netbox.example.com 7 | httpScheme: "http" 8 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config39.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vmRoleRelations: # Wrong vmRoleRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config4.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "httpd" # Invalid value 10 | 11 | source: 12 | - name: testolvm 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: admin@internal 16 | password: adminpass 17 | permittedSubnets: 18 | - 172.16.0.0/12 19 | - 192.168.0.0/16 20 | - fd00::/8 21 | validateCert: true 22 | ignoreVMTemplates: true 23 | 24 | - name: testvcenter 25 | type: vmware 26 | hostname: testvcenter.example.com 27 | username: admin 28 | password: adminpass 29 | permittedSubnets: 30 | - 172.16.0.0/12 31 | 32 | - name: prodvcenter 33 | type: vmware 34 | hostname: prodvcenter.example.com 35 | username: test 36 | password: test 37 | permittedSubnets: 38 | - 10.0.0.0/8 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config40.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | hostRoleRelations: # Wrong hostRoleRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config41.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | datacenterClusterGroupRelations: # Wrong datacenterClusterGroupRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config42.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanTenantRelations: # Wrong vlanTenantRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config43.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanGroupRelations: # Wrong vlanGroupRelations 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config44.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | customFieldMappings: # Wrong custom field mappings 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config45.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | 18 | - 123421334 # Wrong source config 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config46.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | permittedSubnets: 18 | - 192.168.1.24/20 19 | - 172.16.0.1/16 20 | - 172.16.0.1 # Wrong format 21 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config47.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanSiteRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config48.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | httpScheme: "http" 10 | 11 | source: 12 | - name: wrong 13 | type: ovirt 14 | hostname: testolvm.example.com 15 | username: "test" 16 | password: "test" 17 | vlanGroupSiteRelations: 18 | - (wrong() = wwrong 19 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config5.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | 10 | source: 11 | - name: testolvm 12 | type: ovirt 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | permittedSubnets: 17 | - 172.16.0.0/12 18 | - 192.168.0.0/16 19 | - fd00::/8 20 | validateCert: true 21 | 22 | - name: testolvm 23 | type: ovirt 24 | hostname: ovirt.example.com 25 | username: admin 26 | password: adminpass 27 | permittedSubnets: 28 | - 172.16.0.0/12 29 | 30 | - name: prodovirt 31 | type: vmware 32 | httpScheme: httpd # invalid value 33 | hostname: ovirt.example.com 34 | username: test 35 | password: test 36 | permittedSubnets: 37 | - 10.0.0.0/8 38 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config6.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | 10 | source: 11 | - name: testolvm 12 | type: ovirt 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | permittedSubnets: 17 | - 172.16.0.0/12 18 | - 192.168.0.0/16 19 | - fd00::/8 20 | validateCert: true 21 | hostSiteRelations: 22 | - .* = Default 23 | hostTenantRelations: 24 | - .* = Default 25 | vmTenantRelations: 26 | - .* = Default 27 | 28 | - name: testolvm 29 | type: ovirt 30 | hostname: ovirt.example.com 31 | username: admin 32 | password: adminpass 33 | permittedSubnets: 34 | - 172.16.0.0/12 35 | hostSiteRelations: 36 | - .* = Default 37 | hostTenantRelations: 38 | - This should not work 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config7.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | 10 | source: 11 | - name: testolvm 12 | type: ovirt 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | permittedSubnets: 17 | - 172.16.0.0/12 18 | - 192.168.0.0/16 19 | - fd00::/8 20 | validateCert: true 21 | hostSiteRelations: 22 | - .* = Default 23 | hostTenantRelations: 24 | - .* = Default 25 | vmTenantRelations: 26 | - .* = Default 27 | 28 | - name: prodolvm 29 | type: ovirt 30 | hostname: ovirt.example.com 31 | username: admin 32 | password: adminpass 33 | permittedSubnets: 34 | - 172.16.0.0/12 35 | hostSiteRelations: 36 | - .* = Default 37 | hostTenantRelations: 38 | - "[a-z++ = Should not work" 39 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config8.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | 10 | source: 11 | - name: testolvm 12 | type: ovirt 13 | hostname: testolvm.example.com 14 | username: admin@internal 15 | password: adminpass 16 | port: 1111111 # wrong port number 17 | -------------------------------------------------------------------------------- /testdata/parser/invalid_config9.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 4 # Wrong log level 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | -------------------------------------------------------------------------------- /testdata/parser/valid_config1.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "test" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: false 10 | removeOrphansAfterDays: 5 11 | 12 | source: 13 | - name: testolvm 14 | type: ovirt 15 | httpScheme: http 16 | hostname: testolvm.example.com 17 | username: admin@internal 18 | password: adminpass 19 | ignoredSubnets: 20 | - 172.16.0.0/12 21 | - 192.168.0.0/16 22 | - fd00::/8 23 | validateCert: true 24 | tag: testing 25 | tagColor: ff0000 26 | 27 | - name: paloalto 28 | type: paloalto 29 | httpScheme: http 30 | hostname: palo.example.com 31 | username: svcuser 32 | password: svcpassword 33 | ignoredSubnets: 34 | - 172.16.0.0/12 35 | - 192.168.0.0/16 36 | - fd00::/8 37 | collectArpData: true 38 | vlanTenantRelations: 39 | - .* = Default 40 | vlanSiteRelations: 41 | - .* = Default 42 | vlanGroupRelations: 43 | - .* = Default 44 | vlanGroupSiteRelations: 45 | - .* = Default 46 | 47 | - name: prodolvm 48 | type: ovirt 49 | hostname: ovirt.example.com 50 | username: admin 51 | port: 80 52 | password: adminpass 53 | ignoredSubnets: 54 | - 172.16.0.0/12 55 | datacenterClusterGroupRelations: 56 | - .* = Default 57 | clusterSiteRelations: 58 | - Cluster_NYC = New York 59 | - Cluster_FFM.* = Frankfurt 60 | - Datacenter_BERLIN/* = Berlin 61 | hostSiteRelations: 62 | - .* = Berlin 63 | clusterTenantRelations: 64 | - .*Stark = Stark Industries 65 | - .* = Default 66 | hostTenantRelations: 67 | - .*Health = Health Department 68 | - .* = Default 69 | vmTenantRelations: 70 | - .*Health = Health Department 71 | - .* = Default 72 | -------------------------------------------------------------------------------- /testdata/parser/valid_config2.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "warning" 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | 10 | source: 11 | - name: dnacenter 12 | type: dnac 13 | httpScheme: http 14 | hostname: dnac.example.com 15 | username: admin@internal 16 | password: adminpass 17 | wlanTenantRelations: 18 | - .* = EXAMPLE 19 | 20 | - name: prodprox 21 | type: proxmox 22 | hostname: proxmox.example.com 23 | username: admin 24 | port: 80 25 | password: adminpass 26 | -------------------------------------------------------------------------------- /testdata/parser/valid_config3.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: 2 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: False 10 | 11 | source: 12 | - name: coreswitch 13 | type: ios-xe 14 | hostname: core.example.com 15 | username: admin@internal 16 | password: adminpass 17 | 18 | - name: vcenter 19 | type: vmware 20 | hostname: vcenter.example.com 21 | username: admin 22 | password: pass 23 | customFieldMappings: 24 | - Mail = email 25 | - Creator = owner 26 | - Description = description 27 | hostRoleRelations: 28 | - .* = Host ESX 29 | vmRoleRelations: 30 | - .* = Virtual Machine ESX 31 | -------------------------------------------------------------------------------- /testdata/parser/valid_config4.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "error" 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: False 10 | 11 | source: 12 | - name: coreswitch 13 | type: ios-xe 14 | hostname: core.example.com 15 | username: admin@internal 16 | password: adminpass 17 | -------------------------------------------------------------------------------- /testdata/parser/valid_config5.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "Debug" 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: False 10 | 11 | source: 12 | - name: coreswitch 13 | type: ios-xe 14 | hostname: core.example.com 15 | username: admin@internal 16 | password: adminpass 17 | -------------------------------------------------------------------------------- /testdata/parser/valid_config6.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "info" 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: False 10 | 11 | source: 12 | - name: coreswitch 13 | type: ios-xe 14 | hostname: core.example.com 15 | username: admin@internal 16 | password: adminpass 17 | -------------------------------------------------------------------------------- /testdata/parser/valid_config7.yaml: -------------------------------------------------------------------------------- 1 | logger: 2 | level: "warning" 3 | dest: "" 4 | 5 | netbox: 6 | apiToken: "netbox-token" 7 | port: 666 8 | hostname: netbox.example.com 9 | removeOrphans: False 10 | 11 | source: 12 | - name: coreswitch 13 | type: ios-xe 14 | hostname: core.example.com 15 | username: admin@internal 16 | password: adminpass 17 | --------------------------------------------------------------------------------