├── .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] "
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: 
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 < /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 = `
4 |
5 |
6 |
7 | `
8 |
9 | const systemFilter = `
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | `
18 |
19 | const interfaceFilter = `
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | `
37 |
38 | const arpFilter = ``
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 := ""
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@(?.*?)\\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 |
--------------------------------------------------------------------------------